目录
1、面向过程和面向对象的初步认识
2、类的引入
3、类的访问限定符及封装
3.1、访问限定符
3.2、封装
4、类的声明和定义或类的定义(可以理解成声明和定义)
5、类的作用域
6、类的实例化
7、类对象模型
7.1、如何计算类对象的大小
7.2、类对象的存储方式猜测
7.3、内存对齐规则
8、this 指针
8.1、this 指针的引出
8.2、this 指针的特性
8.3、关于 this 指针常见的面试题
1、面向过程和面向对象的初步认识
C 语言是
面向过程
的,
关注
的是
过程
,分析出求解问题的步骤,通过函数调用逐步解决问题、
C++ 是
基于面向对象
的,
关注
的是
对象
,将一件事情拆成不同的对象,靠对象之间的交互完成、
2、类的引入
C++ 引入了类的概念,但是在最初时,是使用关键字 struct 来引入的类,在 C 语言中,声明和定义或定义(可以理解成声明和定义)的结构体中只能 声明 变量,而在 C++ 中,声明和定义或定义(可以理解成声明和定义)的结构体中不仅可以 声明 变量,也可以 声明和定义 函数、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//在C语言中,若声明和定义一个学生的结构体,则如下所示:
//struct Student
//{
// //在C语言中声明和定义结构体的话,结构体成员中只能 声明 变量、
// char _name[20];
// char _gender[3];
// int _age;
//};
//上述代码,若在C语言中就看做是声明和定义的一个结构体,若在C++中则看做是声明和定义了一个类、
//由于C++兼容C语言,所以C++也兼容C语言中的关键字struct的用法,所以在C++中也能够成功执行上述代
//码,只不过是,上述代码在C语言看来就是声明和定义了一个结构体,而在C++看来则是声明和定义了一个类
//,除此之外,在C++中,该类里面不仅仅可以声明变量,也可以声明和定义函数,同时,C++也对C语言中声明
//和定义的struct结构体进行了升级,升级成了类,其主要意义在于:
//1、现C++中的类名Student可以直接作为类型进行使用、
//2、在类里面,除了可以声明变量之外,还可以声明和定义函数、
//在C++中,下述代码则会被视为声明和定义的一个类,其中:struct为类关键字,Student为类名,或者是类标签、
struct Student
{
//声明和定义函数、
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Print()
{
cout << _name << " " << _gender << " " << _age << endl;
}
//声明变量、
//C++中,当在类里面声明变量时,一般在变量名前面或者后面加上_(或者是其他形式,看
//公司,C++并没有规定必须写成什么形式,但Java中有着明确的规定),注意不是必须要加的,主要是用来表
//明这是类里面的成员变量,否则可能在某些地方会出现歧义、
char _name[20];
char _gender[3];
int _age;
};
//注意:上面的类中的变量的声明和函数的声明和定义的位置是随意的,变量上,函数下或者变量下,
//函数上或者任意位置,都是可以的,这是因为:类是一个整体,此时编译器并不是只会向上找,而是在该类这
//个整体里面去寻找,所以上述写法也是可以的,除了类之外,则编译器均默认是向上查找的,那么上述类里
//面的写法就会出现错误,还要知道,在C++中看到上述代码,要把他看做是声明和定义的一个类,不要把他
//看做是声明和定义的一个结构体,而我们所谓的C++兼容C语言中的关键字struct的用法,只不过是在使用
//时可以按照结构体的用法去使用,但本质上已经升级成了类,所以要看做类去处理、
//在C语言中:
//结构体:
//struct ListNode
//{
// int val;
// struct ListNode* next;//不可以写成:ListNode* next,即使加上typedef也是不可以的,如下所示:
//};
//结构体:
//typedef struct ListNode
//{
// int val;
// ListNode* next; //错误写法、
//}ListNode;
//在C++中可以写成如下所示:
//类:类,不是只能声明和定义一个,可以声明和定义多个、
//struct ListNode
//{
// int val;
// //C++兼容C语言中struct的用法:
// struct ListNode* next;//正确写法、
// //新增用法:这是因为C++对C语言中声明和定义的struct结构体升级成了类,下面则是类新增的使用方法:
// ListNode* next; //正确写法、
//};
//在C++中一般不需要再对struct ListNode进行typedef为ListNode,因为类名可以直接当做类型进行
//使用,除非类名过长,若再想对类名进行重命名的话,一般写成下面这种情况:
//一、
//struct ListNode
//{
// int val;
// ListNode* next;
//};
//typedef ListNode ST;//struct ListNode s2; 或者 ListNode s2; 或者 ST s2; 其中: ST s2; === ListNode s2;
//二、
//struct ListNode
//{
// int val;
// ListNode* next;
//};
//typedef struct ListNode ST; //struct ListNode s2; 或者 ListNode s2; 或者 ST s2; 其中: ST s2; === struct ListNode s2;
//将struct ListNode重命名为ST,即:将typedef与最后一个单词(ST)两者中间的内容(struct ListNode)重命名为最后一个单词(ST),并且该单词(ST)中不能存在空格、
//上述方法一般不经常使用,意义不大、
int main()
{
//C++兼容C语言中struct结构体的用法,如下所示:
//struct Student s1; //s1在C++中常称为对象,类声明和定义的对象、
//新增用法如下:
Student s2; //s2在C++中常称为对象,类声明和定义的对象、
//访问类里面声明的变量:
s2._age = 10;
//调用类里面声明和定义的函数;
s2.Init("惠俊明", "男", 25); //初始化、
s2.Print();//打印、 //惠俊明 男 25
return 0;
}
在 C++ 里类的声明、声明和定义与定义(可以理解成声明和定义)中,虽然可以使用关键字 struct 来声明、声明和定义与定义(可以理解成声明和定义)类,但是更喜欢用关键字 class 来代替关键字 struct,那么这两个关键字有什么不同呢,下面再进行阐述、
类的声明和定义 或 类的定义(可以理解成声明和定义):
class className
{
//类体:由类成员函数和类成员变量组成、
}; //一定要注意后面的分号、
class 为类的关键字,className 为类的名字,{ }中为类的主体,注意在结束时后面有一个分号、
类体中的内容称为类的成员:类体中的变量称为类的属性、类的数据、类成员对象或者类成员变量;类体中的函数称为类的方法或者类成员函数、
3、类的访问限定符及封装
3.1、访问限定符
C++ 实现封装的方式:用类将对象的
属性与方法
结合在一块,让对象更加完善,通过访问权限,选择性的将其接口提供给外部的用户使用,类里面的内容并不一定全部是开源的,比如说,开源的内容都设置成公有,则使用关键字 public 加冒号;访问限定符
只
限制在类外面进行的直接访问、
访问限定符说明:
1、
公有 public 修饰的类成员函数和类成员变量,在类外可以直接被访问,而私有 private 修饰的类成员函数和类成员变量,在类外不可以直接被访问、
2、
protected 和 private 修饰的类成员函数和类成员变量,在类外不能直接被访问(此处protected 和 private 是类似的,现阶段认为这两者的功能是一样的,但是在继承中,两者就会出现区别,具体后期再进行讲述)、
3、
访问权限的作用域是从该访问限定符出现的位置开始,直到下一个访问限定符出现的位置为止,类体中的最后一个访问限定符的访问权限作用域是从该访问限定符出现的位置开始,直到 };结束为止、
4、
在 C++ 中,关键字 class 的默认访问权限为私有 private,关键字 struct 默认的访问权限为公有 public (因为 C++ 要兼容 C 语言);两者均可以手动写上访问限定符、
5、
关键字 struct 默认的访问权限为公有 public,但是也可以手动的写上访问限定符,比如手动写上访问限定符私有 private ,则在结构体外面不可以直接被访问、
6、
不管在关键字 class 还是关键字 struct 声明和定义或定义(可以理解成声明和定义)的类中,不是只能有一个访问限定符,而是可以存在多个访问限定符、
注意:
访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别、
面试题:
问题:C++ 中关键字 struct 和关键字 class 的区别是什么?
解答:
C++ 需要兼容 C 语言,所以 C++ 中关键字 struct 可以当成结构体去使用其用法,另外C++ 中关键字 struct 还可以用来声明、声明和定义与定义(可以理解成声明和定义)类,和关键字 class 是一样的,区别是关键字 struct 的类成员默认访问方式是 public,关键字 class 是的成员默认访问方式是 private、
3.2、封装
C++面向对象具有三大特性:
封装、继承、多态
、
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装:
将类的数据和操作数据的方法(类的方法)进行有机结合,隐藏对象的属性和实现细节,仅对外公开某一些接口来和对象进行交互、封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了,那么我们首先建了一座房子把兵马俑给封装起来,但是我们目的全封装起来,不让别人看,所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观;类也是一样,我们使用类的数据和方法都封装到一下,不想给别人看到的,我们使用protected/private 把其封装起来,所以封装本质是一种管理、
//一、
//C语言声明和定义栈结构、
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//在C语言中声明和定义或定义(可以理解成声明和定义)的栈结构的数据和方法是分离的、
struct Stack
{
//栈结构的结构体成员变量/数据/属性、
int* a;
int top;
int capacity;
};
//栈结构操纵数据的方法、
void StackInit(struct Stack*ps)
{
ps->a = NULL;
ps->top = 0; //ps->top = -1;
ps->capacity = 0;
}
void StackPush(struct Stack*ps, int x)
{} //具体实现省略、
int StackTop(struct Stack*ps)
{} //具体实现省略,以此方法为例、
//在C语言中声明和定义或定义(可以理解成声明和定义)的栈结构的数据和方法是分离的,会存在什么样的问题呢?
//太过自由,不能很好的进行管理,如下所示:
int main()
{
//声明和定义一个栈(局部结构体变量)、
struct Stack st;
//初始化栈、
StackInit(&st);
//入栈数据、
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
//取栈顶元素、
//方法一: 规范的方法是调用函数接口来取栈顶元素、
printf("%d\n", StackTop(&st));
//方法二: 不规范的方法、
printf("%d\n", st.a[st.top]);
//方法二也是可以的,但是会在某些情况下存在歧义,比如:top不明确到底是栈顶元素的位置还是栈顶
//元素的下一个位置,此时,使用者就可能存在误用,top不明确到底是栈顶元素的位置还是栈顶元素的下一
//个位置,取决于初始化,若初始化top为0,则正确访问应该写成:printf("%d\n", st.a[st.top-1]);
//若初始化top为-1,则正确写法应是: printf("%d\n", st.a[st.top]); 数据结构只是一个思想,并没
//有规定必须把top初始化为某个值,所以使用方法二是不合适的,即,太过自由,不能很好的进行管理,是不
//好的、
//这是因为方法二的使用和栈结构的结构体成员变量/数据/属性的声明是具有联系的,比如:
//top初始化的值决定了方法二中到底使用那个语句,其次,如果改成链表来实现栈结构的话,那么方法二也
//需要进行改动才可以达到目的、
return 0;
}
//接下来看一下C++是如何解决上述问题的: C++不会存在像上面这种误用的情况,所以不会出现歧义的,这
//是因为C++设计出了类、
//二、
//C++声明和定义栈结构、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//在C++中声明和定义或定义(可以理解成声明和定义)的栈结构的 类的数据和类的方法 是合并的、
//1、类的数据和类的方法一起封装到类里面、
//2、当封装到一起后才可以控制访问权限,这样在C语言中若直接通过 数据 进行访问的话,可能会存在
//歧义,但是若在C++中,把类的方法和类的数据都封装到类中,再把类的数据设置为私有,把类的方法设置
//为公开,这样一来,在类外面就不可以直接访问私有的内容,这样就不可以通过类的数据来进行直接访问,
//就避免了上述可能会出现的歧义、
class Stack
{
//C++的封装本质上就是一种更加严格管理的设计、
//一般情况下,设计类,类成员变量/类的数据/类的属性都是设置成私有或者保护的,类的方法,即类成
//员函数的权限要分情况考虑,若想开源的就设置成公用,若不想开源的就设置成私有或者保护即可,所以类成
//员函数(类的方法)并不是一定为公用也不是一定为私有或保护的,要根据自己的情况进行设定,但类成员
//变量/类的数据/类的属性在一般情况下都会设置成私有或保护的、
private:
void CheckCapacity()
{} //用户没有必须去调用判断是否需要增容的函数接口,所以就设置成私有或者保护的即可、
public:
//栈结构操纵数据的方法,即类的方法如下、
void Init()
{}
void Push(int x)
{}
int Top()
{}
private:
//栈结构的类成员变量/类的数据/类的属性、
int* _a;
int _top;
int _capacity;
};
int main()
{
//声明和定义一个栈、
Stack st;
//初始化栈、
st.Init();
//入栈数据、
st.Push(1);
st.Push(2);
st.Push(3);
//取栈顶元素、
//只能通过调用函数接口来取栈顶元素、
cout << st.Top() << endl;
//错误方法:
//cout << st._a[st._top] << endl;//这是因为在类里面已经把 类的数据 设置为私有,所以在类
//的外面不可以直接进行访问,这样要想成功运行,必须使用上述的方法,所以就避免了出现歧义,这样就不
//需要考虑到底把top初始化为0还是-1了,不需要关心,这就体现出了C++强制的通过定义使得用户在使用
//的时候更加规范,这样一来,不需要考虑栈到底是使用顺序表还是链表来实现的,也不需要考虑top的值到
//底初始化为0还是-1,只需要调用对应的函数接口就可以达到目的,底层的实现过程不需要考虑,都不会影
//响调用函数接口得到的结果,这就叫做低耦合,关联关系低,在C语言中如果只使用方法一的话也可以做到
//低耦合,但是避免不了会有人误用了方法二,这样就会出现问题,而在C++中,已经规定了类似于C语言中方
//法二的用法是错误的,所以就避免了出现歧义的情况关联关系越高,越不好、
return 0;
}
4、类的声明和定义或类的定义(可以理解成声明和定义)
类的声明和定义或类的定义(可以理解成声明和定义):
class className
{
//类体:由类成员函数和类成员变量组成、
}; //一定要注意后面的分号、
class为类的关键字,className为类的名字,{ }中为类的主体,注意在结束时后面有一个分号、
类体中的内容称为类的成员:类体中的变量称为类的属性、类的数据、类成员对象或者类成员变量;类体中的函数称为类的方法或者类成员函数、
在 C 语言中声明和定义或定义(可以理解成声明和定义)的结构体里面声明的变量称为结构体成员变量,结构体数据或者是结构体属性,一般不称为结构体成员对象,因为 C 语言不存在对象这一概念,在结构体外面声明、声明和定义或定义(可以理解成声明和定义)的函数称为结构体方法(操纵结构体数据的方法)、
类里面还可以嵌套声明和定义或定义(可以理解成声明和定义)类,这就是所谓的内部类,在后期会进行阐述;类,不是只能声明和定义或定义(可以理解成声明和定义)一个,可以同时声明和定义或定义(可以理解成声明和定义)多个、
在一些项目中,如果代码较多时,要把调用函数的
声明
和
定义(可以理解为声明和定义)
进行分离(在不同的文件中,分离一定分开,分开不一样分离),其好处在于,比如在 C 语言中的自定义的头文件里面就可以知道:声明和定义或定义(可以理解成声明和定义)的结构体的整体结构是什么,而在 C++ 中声明和定义或定义(可以理解成声明和定义)类的话,就如下所示:
类的两种声明和定义或定义(可以理解成声明和定义)方式:
注意:下面不管是那种方式,类的数据都是在头文件(.h文件)中的类体中进行声明的、
一、
类成员函数的声明和定义放在头文件(.h文件)中的类体里面(即:类成员函数的声明和定义(可以理解成声明和定义)不分离(所谓分离即为在不同的文件中,并且两者不分开写);
需要注意:
类成员函数如果在类体中进行声明和定义(不分离且不分开写)或类成员函数如果在类体中进行声明和定义(可以理解成声明和定义)(不分离但分开写,这种情况不常见,可以忽略这种情况),编译器默认将其当成
内联函数
处理(一般情况下,较短的函数并且函数体内不存在循环/递归的时候,会在类体内进行声明和定义,但不是强制规定,而较长的函数或函数体内存在递归或循环等,一般采取方法二),一般情况下,若我们直接说声明和定义,指的则是声明和定义(可以理解成声明和定义)不分离也不分开;
//类成员函数在类内进行声明和定义、
//Stack.h文件
#pragma once
class Stack
{
public:
//类成员函数的声明和定义:
void Init() //不需要再指定类域、
{
//从类内进行的访问、
_a = nullptr;
_top = 0;
_capacity = 0;
}
void Push(int x)//不需要再指定类域、
{}
void Pop()//不需要再指定类域、
{}
private:
int* _a;
int _top;
int _capacity;
};
二、
类成员函数的声明在头文件(.h文件)中的类体里,类成员函数的定义(可以理解成声明和定义)在源文件(.cpp文件)中,即:类成员函数的声明和定义(可以理解成声明和定义)分离(在不同的文件中):
//类成员函数在 类外 进行定义(可以理解成声明和定义)、
//Stack.h文件
#pragma once
class Stack
{
public:
//类成员函数的声明:
void Init();
void Push(int x);
void Pop();
private:
int* _a;
int _top;
int _capacity;
};
//Stack.cpp文件
#include"Stack.h"
//类成员函数的定义(可以理解成声明和定义):
void Stack::Init() //指定类域、
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
//访问限定符限制的是从 类外 进行的直接访问,此处在类成员函数Init的定义(可以理解成声明和
//定义)里面直接访问类成员变量,即类成员函数访问类成员变量不属于从类外进行的直接访问(属于从类内
//进行的直接访问),因为,虽然该类成员函数的定义(可以理解成声明和定义)在类外,但是该类成员函数的
//声明在类内,此处我们要以声明所在的位置为准,所以,即使该类成员函数的定义(可以理解成声明和定义)
//在类外,但其仍是类成员函数,而该类成员函数的函数体内中的所有内容均属于该类成员函数,所以,该类
//成员函数访问类成员变量就属于是从类内进行的直接访问,所以这种情况下,访问限定符不起任何作用、
//访问限定符限制的是从类外进行的直接访问,但是限制不了从类外进行的间接访问,具体在后面进行
//阐述,并且访问限定符不限定从类内进行的直接访问,且不考虑从类体内进行的间接访问;从类内可以任意
//直接访问到私有、保护和公用的内容、
void Stack::Push(int x) //指定类域、
{}
void Stack::Pop() //指定类域、
{}
注意:可以采用方式一,也可以采用方式二,也可以方式一和二混用,但在一般情况下,更期望采用第二种方式;这是常见的两种方式,还有其他方式,比如把方式一中声明和定义的类的整体内容放在 test.cpp 里面的 main 函数的上面也是可以的、
5、类的作用域
类设有了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义(可以理解成声明和定义)成员,需要使用 :: 作用域解析符指明该成员属于哪个类域、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//类域、
//此时并没有为此处声明和定义的类体分配内存空间、
class Person
{
public:
void PrintPersonInfo(); //声明、
private:
char _name[20]; //仅仅是声明,没有为其开辟内存空间、
char _gender[3];
int _age;
};
//这里需要指定PrintPersonInfo是属于Person这个类域、
void Person::PrintPersonInfo() //定义(可以理解成声明和定义)、
{
cout << _name << " "<<_gender << " " << _age << endl;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//类,不是只能声明和定义一个,可以同时声明和定义多个,如下所示:
//Stack类域、
class Stack
{
public:
void Push(int x)
{}
};
//Queue类域、
class Queue
{
public:
void Push(int x)
{}
};
//这两个Push函数可以同时存在,这是因为处于不同的作用域内,在不同的作用域内可以同时存在函数名相同
//且形参列表相同的函数,以及相同的变量名;
//上述的两个Push函数不构成函数重载(在同一个作用域内,函数名相同,但形参列表必须不同)、
知识拓展:
6、类的实例化
头文件( .h 文件 )中的类体里面的类成员变量(包括 C 语言中,结构体里面的结构体成员变量)只是声明,并不是定义(可以理解成声明和定义);
一般情况下,变量或函数的声明和定义(可以理解成声明和定义)的区别在于:是否为其开辟了内存空间,当使用类名去声明和定义对象时(或者是在 C 语言中当使用结构体类型声明和定义变量时),才算是对头文件( .h文件 )中的类体里面的类成员变量( C 语言中,结构体里面的结构体成员变量)进行了定义(可以理解成声明和定义),才为其开辟了内存空间;
函数的声明和定义(可以理解成声明和定义)的区别也是在于是否为其开辟了内存空间,其声明是不开辟内存的,仅仅告诉编译器,要声明的部分存在,要预留一点空间,定义(可以理解成声明和定义)则需要开辟内存,在定义(可以理解成声明和定义)函数时,不管是全局函数还是类成员函数,均是以函数体二进制代码的形式在内存中的公共代码区上为其开辟内存空间、
用类的类型创建对象的过程,称为类的实例化:
1、
类只是一个模型一样的东西,是一种自定义的数据类型,
系统并不为类分配存储空间
,其限定了类有哪些成员,
声明和定义或定义(可以理解成声明和定义)出一个类,并没有分配实际的内存空间来存储它,在 C 语言中声明和定义或定义(可以理解成声明和定义)出一个结构体,也没有分配实际的内存空间
,在通常情况下,我们讨论 "分配内存空间" 时,指的仅仅是数据,对于代码和语义的东西,那完全是不同的概念,比如说函数,他本来由高级语言编写,编译后只不过是转换成机器语言代码,保存在程序中而已,其它没什么实质变化,程序运行时,所有的机器语言代码都调入内存,用到谁就一条条地执行谁,所以,上述所谓的并没有分配实际的内存空间来存储它,指的只是类体里面的类的数据,并没有指类体中的类的方法,在类体中既可以声明类的数据,也可以声明和定义类的方法,类的方法是以函数体二进制代码的形式存放在内存中的公共代码区上的,类的数据(即类成员变量,它不属于局部变量,也不属于全局变量),目前来说,不考虑 C 语言中在结构体成员变量前面加上关键字 static 的情况,类本身并不可以存储数据,但类声明和定义出或者是实例化出的对象就可以存储数据了,C 语言中
声明和定义或定义(可以理解成声明和定义)
的结构体本身不可以存储数据,但是由结构体类型声明和定义出的结构体变量就可以存储数据了、
2、
一个类可以实例化出多个对象,
实例化出的对象占用实际的物理内存空间,用来存储类成员变量、
3、
做个比方,类实例化出的对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理内存空间、
4、
所谓的计算所占内存空间的大小,包括占用的内存空间中的所有的区,
而在计算类实例化出来的对象的大小时,并没有计算类中的类成员函数(包括静态和非静态类成员函数)所占的内存大小
,是因为:类成员函数(包括静态和非静态类成员函数)并不存在于类实例化出来的对象里面,类成员函数(包括静态和非静态类成员函数)是共用的,假设有一个调用函数为:void HjmLcc() { },该调用函数可以作为非静态类成员函数,也可以作为非静态全局函数,只不过是,当作为非静态类成员函数时仅仅比作为非静态全局函数时,在形参列表中会多出一个 this 指针而已,其他地方则没有区别,类实例化出来的对象里面无非就是对类的数据进行的实例化,函数不管是全局函数(包括非静态和静态)还是类成员函数(包括非静态和静态),均是以函数体二进制代码的形式在内存中的公共代码区上为其开辟内存空间,这两者都是在进行调用时才会在
栈区
上建立栈帧、
5、
类的静态类成员变量在类声明和定义或定义(可以理解成声明和定义)时就已经在全局数据区为其分配了内存空间,因而它是属于类的,对于非静态类成员变量,是在类的实例化过程中(构造对象)才在栈区,数据段(静态区或全局数据区)或者堆区为其分配内存空间,是为每个对象生成一个拷贝,所以它是属于对象的、
6、
应当说明,常说的 "某某对象的类成员函数" ,是从逻辑的角度而言的,而类成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的、
7、
静态类成员函数和非静态类成员函数的区别:静态类成员函数和非静态类成员函数都是在类的声明和定义或定义(可以理解成声明和定义)时放在内存的公共代码区的,因而可以说它们都是属于类的,但是类为什么只能直接调用静态类成员函数,而非静态类成员函数(即使该函数看起来没有参数)只有类对象才能调用呢?原因是类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即 this 指针),因而只有类对象才能调用(此时 this 指针有实值)、
8、
类成员函数的代码段都用同一种方式存储,不要将类成员函数的这种存储方式和inline(内联)函数的概念混淆,不要误以为用 inline声明(或默认为inline)的类成员函数,其代码占用对象的存储空间,而不用 inline声明的类成员函数,其代码不占用对象的存储空间,不论是否用 inline声明(或默认为inline),类成员函数的代码都不占用对象的存储空间,用 inline声明的作用是在调用该函数时,将函数的代码复制插入到函数调用点,而若不用 inline声明,在调用该函数时,流程转去函数代码的入口地址,在执行完该函数代码后,流程返回函数调用点,inline与类成员函数是否占用对象的存储空间无关,它们不属于同一个问題,不应搞混、
静态类成员:
静态类成员就是在类成员变量或类成员函数前面加上关键字 static ,称为静态类成员,静态类成员分为:静态类成员变量和静态类成员函数、
静态类成员变量:在类成员变量前面加上关键字 static 、
1、
均共享同一份该静态类成员变量中存储的数据,即:该静态类成员变量中存储的数据当通过某一途径访问到的值假设为 int 整型100,若通过其他途径再次进行访问时,访问到的值仍为 int 整型100,多个途径访问到的数据是同一个数据,静态类成员变量是不属于对象的,而是属于类的,关键字 static 修饰的类成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为静态类成员变量分配一份内存,所有对象使用的都是这份内存空间中存储的数据,当某个对象修改了该静态类成员变量中存储的数据时,也会影响到其他对象、
2、
在编译阶段分配内存空间,并不是在创建对象之后才在栈区(用类名实例化出普通局部对象),数据段(静态区,用类名实例化出普通全局对象或静态对象)或者堆区(动态开辟)分配的内存空间,相当于在程序运行之前就已经分配了内存空间,而能够在程序运行之前就可在内存中分配内存空间的区域只有:公共代码区和全局数据区,所以是在编译阶段在全局数据区内为其分配内存空间、
3、
在类内声明,在类外进行定义(可以理解成声明和定义),并且两者都必须要进行之后才可以去使用该静态类成员变量,否则不能使用,若只在类内进行声明,并未在类外进行定义(可以理解成声明和定义),则不管是访问还是修改该静态类成员变量中的值,都会报一个链接错误,所以,这两者必须都要进行后才不会报错,要注意:当在类外进行定义(可以理解成声明和定义)时,要进行分类讨论:
(1)、如果所有的代码均在 test.cpp 文件中的话,静态类成员变量在类外进行定义(可以理解成声明和定义)要保证不能在 main 函数中进行,还要保证必须在类体外的下面进行,在满足这些条件的前提下,可在任意位置进行静态类成员变量的定义(可以理解成声明和定义)、
(2)、若把类声明和定义在头文件 Stack.h 中,并且静态类成员变量的声明在类体中,静态类成员变量的定义(可以理解成声明和定义)则可以在源文件 Stack.cpp 中进行,除此之外,静态类成员变量的定义(可以理解成声明和定义)也可以放在头文件 Stack.h 中,但还要保证在类体外的下面才可以,原因同上,静态类成员变量的定义(可以理解成声明和定义)和初始化的格式见代码所示,在类外进行定义(可以理解成声明和定义)时,可以赋初值,也可以不赋值,即可以初始化,也可以不初始化,但一定要进行定义(可以理解成声明和定义),如果不赋值,那么会被默认为 0;当定义(可以理解成声明和定义)和声明不分离且不分开写的时候,若在全局区域进行声明和定义,则属于全局变量,且不初始化默认为 0,若在局部区域进行声明和定义,则属于局部变量,且不初始化默认为随机值,而对于静态类成员变量而言,其,是在类内声明,在类外定义(可以理解成声明和定义),并且只能在类外的全局区域中进行定义(可以理解成声明和定义),此时不管是否分离,由于在类外的全局区域进行的定义(可以理解成声明和定义),故,不初始化则默认为 0,但由于静态类成员变量的声明是在类内的,所以,仍属于静态类成员变量,而不属于全局变量、
4、
静态类成员变量也具有访问权限,静态类成员变量在内存中(全局数据区)只有一份,非静态类成员变量在内存中(栈区,静态区或堆区)不一定只有一份,可能会存在多份、
5、
静态类成员变量在类外进行定义(可以理解成声明和定义)和初始化时不能再加关键字 static ,但必须要有数据类型,被 private、protected、public 修饰的静态类成员变量都可以用这种方式初始化、
6、
关键字 static 修饰的类成员变量的内存既不是在类内声明时分配,也不是在创建对象时分配,而是在(类外)进行定义(可以理解成声明和定义)和初始化时分配,反过来说,没有在类外进行定义(可以理解成声明和定义)和初始化的被关键字 static 所修饰的类成员变量不能使用、
在 C++11 中,不可以在类体中的静态类成员变量(对象)声明处赋缺省值,因为这里的缺省值是用于初始化列表中进行赋值(初始化)的,而静态类成员变量(对象)是在类外进行定义(可以理解成声明和定义)的,并不是在初始化列表中进行的定义(可以理解成声明和定义)、
只能在类外的静态类成员变量(对象)的定义(可以理解成声明和定义)处赋值(初始化),若在类外只进行静态类成员变量(对象)的定义(可以理解成声明和定义),而不对其赋值(初始化),则该静态类成员变量的值默认为 0 、
7、
关键字 static 修饰的类成员变量不占用对象的内存,而是在所有对象之外开辟内存(全局数据区),即使不创建对象也可以访问,具体来说,static 类成员变量(静态类成员变量)和普通的 static 变量(静态变量)类似,都在内存分区中的全局数据区分配内存,并且都仅有一份,到程序结束时才释放,这就意味着,static 类成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存,而普通的类成员变量在对象创建时分配内存,在对象销毁时释放内存、
8、
一个类体中可以有一个或多个静态类成员变量,均共享这些静态类成员变量,都可以引用它、
9、
静态类成员变量属于整个类,属于由该类实例化出来的所有对象,并不是只属于某一个对象,存在于静态区、
10、
静态类成员变量必须在类外定义(可以理解成声明和定义),定义时不添加 static 关键字、
11、
静态类成员(包括静态类成员变量和静态类成员函数)为所有的类对象所共享,不属于某个具体的实例、
12、静态类成员变量也具有访问权限、
拓展:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
static const int a=100;//声明和定义,赋值(初始化)、
//静态const整型(char,int,short,long,long long等)类成员变量和静态const枚举类型的类成员变量
//可以在类体内直接声明和定义并赋值(初始化),这是因为他们在编译时就可以确定其值,因此编译器会
//在编译期将他们的值直接嵌入到生成的代码中,这意味着它们不需要在类外分配额外的存储空间,这种
//特殊情况只适用于整型和枚举类型的静态const类成员变量,其他类型的静态const类成员变量仍然需要
//在类外进行赋值(初始化),即其只能在类内声明,在类外定义(可以理解成声明和定义)、
};
int main()
{
cout << Date:: a << endl;
return 0;
}
#include <iostream>
using namespace std;
//静态类成员变量:
class Person
{
public:
//int m_A; //非静态类成员变量,只是声明、
static int _mA;//静态类成员变量,只是声明,类内声明、
//静态类成员变量也具有访问权限、
private:
static int _mB; //静态类成员变量,只是声明,类内声明、
};
int Person::_mA = 100; //类外定义(可以理解成声明和定义)和初始化,要指定类域、
//类外定义(可以理解成声明和定义)和初始化,但不能在main函数中进行,还要保证在 类体外 的下面进行
//,因为使用到了 Person::,若在 类体外 上面进行的话,系统会向上查找 Person:: ,就找不到,所以会
//报错,在满足上述条件的基础下,可以在任意位置进行静态类成员变量的定义(可以理解成声明和定义)和
//初始化,其格式如上所示、
//上述在进行静态类成员变量的定义(可以理解成声明和定义)和初始化时,必须要进行指定类域,若写成:
//int _mA = 10;的话,这代表的是声明和定义全局变量,这样就看不出来是对类体中的静态类成员变量进
//行的定义(可以理解成声明和定义)和初始化,所以必须要指定类域才可以、
//类外定义(可以理解成声明和定义)和初始化:
int Person::_mB = 200;
void test01()
{
Person p;
cout << p._mA << endl; //100
Person p2;
p2._mA = 200;
cout << p._mA << endl;//200
}
void test02()
{
//非静态类成员变量属于对象,而不属于类,所以其访问形式只能通过对象或通过匿名对象突破类域进
//行访问,不可以通过类名加作用域限定符的形式进行访问、
//此处的静态类成员变量_mA为公有,可以在类外直接访问、
//静态类成员变量不属于对象,而属于类,其访问形式有:
//1、通过对象进行访问静态类成员变量:
Person p;
cout << p._mA << endl; //100
//2、通过类名加作用域限定符的形式进行访问静态类成员变量,也可以不用创建对象再使用对象来进
//行访问,因为静态类成员变量本身就不属于对象,而属于类、
cout << Person::_mA << endl; //100
//3、
cout << Person()._mA << endl; //通过匿名对象突破类域进行访问、
//cout << Person::_mB << endl; //错误写法,在类外不可以直接访问私有的静态类成员变量、
}
int main()
{
//test01();
test02();
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
//定义一个自定义类型的类,类名为A,计算程序中创建(实例化)出了多少个 A 类型的对象、
//直接数的话可能不准确,因为会涉及到编译器的优化, A 类型的对象可能是调用一次拷贝构造函数出来的
//也有可能是我们自己创建出来的,此时他会自动调用一次构造函数,即,实例化出一个 A 类型的对象,那么
//就一定调用一次构造函数或者是调用一次拷贝构造函数,所以我们只需要统计一下,调用构造函数和调用
//拷贝构造函数的总共的次数,得到的就是创建出 A 类型的对象的个数、
//int count = 0; //全局变量,可能会存在命名冲突的问题、
//但是使用全局变量也会存在一些问题,比如,全局变量用在项目中,若全局变量声明和定义在.h头文
//件的话,当在多个.cpp源文件中包含头文件后,就会出现链接冲突,和在头文件中声明和定义全局函数造
//成的问题类似,也可以使用全局变量的定义(可以理解成声明和定义)和声明分离的方法,但是,会比较麻烦
//,并且C++编译器从封装的角度而言并不提倡使用全局变量,因为别处也能够操作该全局变量,此时就可以
//使用静态类成员变量,封装性也比较好、
class A
{
public:
A()
{
_count++;
}
A(const A& aa)
{
_count++;
}
static int Get_count()
{
return _count;
}
//静态类成员函数,属于整个类,即属于由该类实例化出来的所有对象,不属于某一个对象,且形参列表
//中的第一个位置上没有隐藏的this指针、
private:
static int _count; //静态类成员变量在类内声明、
};
//静态类成员变量在类外进行定义(可以理解成声明和定义)、
int A::_count = 0;
//自定义类型的对象 传值 传参和 传值 返回、
A Func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2=Func(a1);
//cout << sizeof(a1) <<endl;
//此时不考虑类体中静态类成员变量所占的内存空间,因为静态类成员变量属于整个类,属于由该类实
//例化出来的所有的对象,而不属于某一个对象、
//注意:在计算时不要把静态类成员(主要指的是静态类成员变量)算进去、
//非静态类成员变量属于某一个对象,所以在类外只能通过对象或通过匿名对象突破类域去访问,而静
//态类成员变量属于整个类,即属于由该类实例化出来的所有的对象,故,在类外可以通过类名加作用域限定
//符的形式去访问,也可以通过对象或通过匿名对象突破类域去访问,在此默认类体中的类成员变量是公
//有的、
//cout << A::_count << endl;
//cout << a1._count << endl;
//cout << a2._count << endl;
//cout << A()._count << endl; //5、
//非静态类成员函数虽然属于类,但是在类外只能通过对象或通过匿名对象突破类域去访问,静态类成
//员函数属于整个类,即属于由该类实例化出来的所有的对象,故,在类外可以通过类名加作用域限定符的形
//式去访问,也可以通过对象或通过匿名对象突破类域去访问、
//当使用对象进行访问时,并不是传某一个对象的地址给该静态类成员函数形参中第一个位置上隐藏
//的this指针,因为静态类成员函数形参列表中不存在隐藏的this指针,而是通过所使用的对象来告诉编译
//器去类体中查找该静态类成员函数,此时类体中的类成员变量是私有的、
cout << a1.Get_count() << endl; //5、
cout << a2.Get_count() << endl; //5、
cout << A::Get_count() << endl; //5、
cout << A().Get_count() << endl; //6、
return 0;
}
静态类成员函数:在类成员函数前面加上关键字 static 、
1、
均共享同一个静态类成员函数,静态类成员函数属于类,不属于对象、
2、
静态类成员函数只能访问静态类成员变量,不能访问非静态类成员变量,非静态类成员函数可以访问静态和非静态类成员变量、
3、
静态类成员函数在内存中(公共代码区)中只有一份,非静态类成员函数在内存中(公共代码区)中也只有一份,平常常说的类成员函数和类成员变量是分别默认为非静态类成员函数,非静态类成员变量;一个类体中可以有一个或多个静态类成员函数和非静态类成员函数,均共享这些静态类成员函数和非静态类成员函数,都可以引用它、
4、
静态类成员函数也具有访问权限,也可以具有返回值、
(1)、当所有的代码均在源文件 test.cpp 中时,静态类成员函数和非静态类成员函数均可在类体中进行声明和定义,除此之外,只在类体中进行声明的话,而定义(可以理解成声明和定义)要在类体外的话,不能把定义(可以理解成声明和定义)放在其他函数内部,只能在其他函数外部,即全局中来进行定义(可以理解成声明和定义),这是因为函数不能嵌套定义(可以理解成声明和定义),只能嵌套调用,但是还要保证放在类体外的下面,原因同上,在保证这里条件的基础下,在任意位置进行静态类成员函数和非静态类成员函数的定义(可以理解成声明和定义)都是可以的、
(2)、若把类定义在头文件 Stack.h 中,并且只在类体中进行静态类成员函数和非静态类成员函数的声明,则静态类成员函数和非静态类成员函数的定义(可以理解成声明和定义)可以在源文件 Stack.cpp 中进行,除此之外,静态类成员函数和非静态类成员函数的定义(可以理解成声明和定义)也可以放在头文件 Stack.h 中,但还要保证在类体外的下面才可以,原因同上,除此之外,若把类定义在头文件 Stack.h 中的话,把静态类成员函数和非静态类成员函数的声明和定义(可以理解成声明和定义)均放在类体中也是可以的,不管是静态类成员函数还是非静态类成员函数,都可以把定义(可以理解成声明和定义)放在类外,而静态类成员变量的定义(可以理解成声明和定义)必须在类外,非静态类成员变量的定义(可以理解成声明和定义)是在由类名实例化对象的过程中进行定义(可以理解成声明和定义)的,静态类成员函数和非静态类成员函数的定义(可以理解成声明和定义)的格式是一样的,见代码所示、
6、
静态类成员函数可以调用非静态类成员函数吗?不可以,因为静态类成员函数没有隐藏的 this 指针,无法调用非静态类成员函数、
7、
非静态类成员函数可以调用类的静态类成员函数吗?可以,因为静态类成员函数为所有类对象所共享、
#include <iostream>
using namespace std;
//静态类成员函数:
//1、均共享同一个静态类成员函数、
//2、静态类成员函数只能访问静态类成员变量,不能访问非静态类成员变量、
class Person
{
public:
//void func() //非静态类成员函数的声明和定义、
//{}
static void func() //静态类成员函数的声明和定义、
{
_mA = 100;
//类体内的类成员函数访问类成员变量不受访问限定符的限制,限定符只限制从类外进行的直接访问,
//类成员函数可以直接访问类成员变量,类体内的一切对于类成员函数而言都是透明的,所以上述操作是可
//以的,即使静态类成员变量_mA是私有的,也是可以的,这就是所谓的静态类成员函数可以访问静态类成员
//变量、
cout << "static void func调用" << endl;
//_mB = 200;
//报错,静态类成员函数不可以访问非静态类成员变量,原因是:静态类成员函数的形参列表中不
//存在隐藏的 this 指针,首先,该静态类成员函数在内存中(公共代码区)只有一份,非静态类成员变量属
//于对象,而不属于类,所以其访问形式只能通过对象进行访问,所以要先创建对象,再通过该对象来访问该
//非静态类成员变量,而静态类成员函数也是共用的,大家共享同一份静态类成员函数,比如由该类名
//Person实例化出来了两个对象分别为:P1和P2,然后再通过这两个对象调用同一个静态类成员函数func来
//访问该非静态类成员变量_mB,而该函数体(不管是静态类成员函数还是非静态类成员函数)中并不能区分
//不同的对象,虽然对象P1和P2在调用这个静态类成员函数func,但是该函数体内并不能区分不同的对象,
//所以静态类成员函数不可以访问非静态类成员变量,因为静态类成员函数中并没有内含 this 指针,但是
//在非静态类成员函数中是可以访问非静态类成员变量的,即使其非静态类成员函数的函数体内不能区分不
//同的对象,但非静态类成员函数中都内含了一个 this 指针,所以也可以达到目的,而这里为什么静态类
//成员函数可以访问静态类成员变量,是因为:静态类成员变量_mA并不属于对象,而是属于类,该静态类成
//员变量_mA是共享的,大家均共用同一份静态类成员变量_mA,所以在该静态类成员函数的函数体内并不需
//要区分该静态类成员变量_mA到底属于哪个对象,因为静态类成员变量_mA根本就不属于对象,而属于类,
//大家都共享同一份静态类成员变量_mA,不需要来区分该静态类成员变量_mA到底属于哪个对象,所以,静
//态类成员函数是可以访问静态类成员变量的、
}
//静态类成员变量、
static int _mA;//静态类成员变量,只是声明,静态类成员变量要在类内声明、
//非静态类成员变量、
int _mB;//非静态类成员变量,只是声明、
//静态类成员函数也具有访问权限、
private:
static void func2()
{
cout << "static void func2调用" << endl;
}
};
//静态类成员变量要在类外定义(可以理解成声明和定义)和初始化、
int Person::_mA = 0; //静态类成员变量在类外定义(可以理解成声明和定义)和初始化,要指定类域、
void test01()
{
//非静态类成员函数也属于类,而不属于对象,但是由于非静态类成员函数其实都内含了一个指向类对
//象的指针型参数(即 this 指针),所以即使它属于类,但是其访问形式也只有两种,即只有通过对象或通
//过匿名对象突破类域来访问非静态类成员函数(此时 this 指针有实值),而不可以通过下述的方式2进行
//访问非静态类成员函数、
//静态类成员函数不属于对象,而属于类,所以它有三种访问形式:
//1、通过对象进行访问静态类成员函数、
//Person p;
//p.func(); //static void func调用
//2、通过类名加作用域限定符的形式进行访问静态类成员函数,也可以不用创建对象再使用对象来进
//行访问,因为静态类成员函数本身就不属于对象,而属于类、
Person::func(); //static void func调用
//3、通过匿名对象突破类域进行访问静态类成员函数、
//Person().func(); //static void func调用
//1、
//Person p;
//cout << p._mA << endl;//100
//2、
//cout<< Person::_mA << endl;//100
//3、
//cout << Person()._mA << endl;//100
//1、
//Person p1;
//p1.func2(); //报错,在 类外 不可以直接访问私有的静态类成员函数、
//2、
//Person::func2();//报错,在 类外 不可以直接访问私有的静态类成员函数、
//3、
//Person().func2();//报错,在 类外 不可以直接访问私有的静态类成员函数、
}
int main()
{
test01();
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
Date(int h = 10)
{
_h = h;
}
static void Print(); //静态类成员函数的声明、
private:
int _h;
};
void Date::Print() //静态类成员函数的定义(可以理解成声明和定义),要指定类域且不加关键字static、
{
cout << "静态类成员函数" << endl;
}
int main()
{
Date d1;
d1.Print();
return 0;
}
注意:在此不考虑 C 语言中在结构体成员变量前面加关键字 static 的情况,所以当说到静态成员时,就默认指的是 C++ 中类体里面的静态类成员变量和静态类成员函数、
类的定义不能像下面这样进行定义,否则在实例化的时候会出现错误:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
Person p;
};
在实例化的过程中将会无穷无尽,这是被 C++ 语法所不允许的,结论:在类的定义中,类成员变量不能使用当前类名作为其类型、
7、类对象模型
7.1、如何计算类对象的大小
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
问题:类体中既可以有类成员变量,又可以有类成员函数,那么一个类的对象中包含了什么?如何计算一个类的对象所占内存空间的大小呢?
7.2、类对象的存储方式猜测
class stu
{
public:
void SetStuInfo()
{}
void PrintInfo()
{}
//类成员函数以其地址的形式进行存储、
private:
char _name[10];
int _age;
char _sex[5];
};
一:对象中包含类的所有成员:
当计算由类实例化出来的对象所占内存空间的大小时,若对象中包含类的所有的成员,包括类的成员函数时,那么在计算时,就默认类的成员函数以其地址(4/8byte)进行存储:
缺陷:每个对象中类成员变量所存储的值是不同的,但是调用同一份类成员函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码(类成员函数的地址),相同代码(类成员函数的地址)保存多次,浪费空间,但这并不是说方式一不可以,它是可以的,但不太好,那么如何解决呢?
二:对象中只保存类的成员变量,类的成员函数存放在公共的代码段:
对于上述两种方式,计算机采用的是第二种方式,所以当计算类实例化出来的对象所占内存空间大小的时候,由于对象中只保存了类成员变量(虚函数指针和虚基类指针也属于数据部分),而未保存类成员函数,把类成员函数存放在了公共的代码段上,类实例化出的所有对象都使用同一份类成员函数,也就是说在内存中的公共代码区上只有一份拷贝,各个对象通过各自的 this 指针对他们进行访问,从而节约内存空间,所以,计算类实例化出来的对象所占内存空间大小就等价于只计算类的成员变量所占内存空间的大小,忽略(不计算)类成员函数所占内存空间的大小,其计算方法和 C 语言中计算结构体变量所占内存空间大小是一样的,也遵循内存对齐的原则,所以,一个类对象中只包含了类成员变量,并没包含类成员函数、
我们再通过对下面的不同对象分别获取所占内存空间大小来分析看下:
//类体中既有类成员变量,又有类成员函数、
class A1 //8byte
{
public:
void f1(){}
private:
int _a;
char _ch;
};
//类体中仅有类成员函数、
class A2 //1byte
{
public:
void f2() {}
};
//类中什么都没有---空类、
class A3 //1byte
{};
//sizeof(A2)和sizeof(A3)结果应该是一样的,两者都没有类成员变量,按理说结果应该都是0byte,但是,
//当类体里面不存在类的成员变量时,系统会给它开辟1byte的内存空间,这是因为,若某个类型所占内存空
//间大小为0byte的话,由该类型定义的实例化出的对象的地址就是空地址,这样就没办法表示该实例化出
//的对象存在过,这1byte不是为了存储类成员变量的,而是为了表示实例化出的对象存在过,即,没有类成
//员变量的类对象,编译器会给他们分配1byte用来占位,表示实例化出的对象存在过,即使考虑上内存对齐
//,最后的结果也是1byte、
//类体中仅有类成员变量、
class A2 //1byte,这1byte就是用来存储类成员变量_ch的、
{
char _ch;
};
结论:
一个类的大小,实际就是该类中 "类成员变量" 所占内存空间之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类、
7.3、内存对齐规则
1、第一个类成员变量在与结构体(或类体)偏移量为 0 (下标)的地址处、
2、其他类成员变量要对齐到某个数字(对齐数)的整数倍(下标)的地址处,
3、注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,VS 中默认的对齐数为 8 、
4、结构体(或类体)总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍(下标)、
5、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍(下标)处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍(下标)、
面试题:
1、结构体怎么对齐? 为什么要进行内存对齐?
2、如何让结构体按照指定的对齐参数进行对齐?
3、什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
8、this 指针
8.1、this 指针的引出
我们先来定义一个类名为 Date 的日期类 Date 、
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
//此处调用的类成员函数Init是同一份类成员函数(公共代码区),此处调用的类成员函数Print是同一
//份类成员函数(公共代码区)、
d1.Init(2022, 5, 11);
d2.Init(2022, 5, 12);
d1.Print(); //2022-5-11
d2.Print(); //2022-5-12
return 0;
}
观察上述打印出来的结果,考虑,在此处,两次调用的类成员函数 Print 是同一份类成员函数(公共代码区),而观察类体内的类成员函数 Print 的声明和定义可知,虽然通过对象 d1 和 d2 来调用该类成员函数 Print ,但是该类成员函数 Print 的函数体内并不能进行区分对象,那么该类成员函数 Print 是怎么来打印出正确匹配的结果呢?
即:当通过对象 d1 调用类成员函数 Print 时,该类成员函数 Print 是如何知道应该打印对象d1 中的类成员变量,而不是打印对象 d2 中的类成员变量的呢,同理,两次调用的类成员函数 Init是同一份类成员函数(公共代码区),而观察类体内的类成员函数 Init 的声明和定义可知,虽然通过对象 d1 和 d2 来调用该类成员函数 Init ,但是该类成员函数 Init 的函数体内也不能进行区分对象,(一般情况下,不管是全局函数(包括静态和非静态)还是静态或非静态类成员函数,其函数体内都不能进行对象的区分),那么该类成员函数 Init 是怎么来初始化出正确匹配的结果的呢,即当通过对象 d1 来调用类成员函数 Init 时,该类成员函数 Init 是如何知道应该设置对象的 d1 中的类成员变量,而不是设置对象 d2 中的类成员变量的呢,此时,这两个类成员函数均不属于某一个对象,而是属于类,这是因为,每个非静态类成员函数内部均内含了一个 this 指针,上述代码中的两个类成员函数会被编译器进行处理,如下所示:
C++ 中通过引入 this 指针解决该问题,即:C++ 编译器给每个 "非静态类成员函数" 增加了一个隐藏的 this 指针参数,让该指针指向当前对象(该非静态类成员函数运行时调用该非静态类成员函数的对象),在非静态类成员函数的函数体中对所有的类成员变量的操作,都是通过该指针去访问的,只不过所有的操作对用户是透明的,即,用户不需要来传递,编译器自动完成、
//隐含的 this 指针、
//每一个非静态类成员函数中都内含了一个this指针,但是每一个非静态类成员函数的形参和实参部分都不能
//显式的写出来有关this指针的内容,因为这是隐含的this指针,是编译器做的活,但是在把非静态类成员
//函数的声明和定义(可以理解成声明和定义)都放在类体里面的前提下,是可以且 只能 在该非静态类成员函
//数的 函数体 内显式的使用this指针变量的,此时该非静态类成员函数的形参和实参部分仍不能显式的
//写出this指针的内容,因为并不属于该非静态类成员函数的函数体、
#include <iostream>
using namespace std;
class Date
{
public:
//const放在了this的左边,修饰的是this指针变量,其本身不能被修改,但是this指针变量所指的内
//容是可以被改变的、
void Print(Date* const this)
//该行代码不可以这样写,即:非静态类成员函数的形参部分不能显式的写出有关this指针的内容,
//this是一个形参指针变量,也是一个新增的关键字、
{
cout << this << endl;//该行代码可这样写,不会报错,因为在满足上述的前提下,可以且只能
//在该非静态类成员函数的函数体内显式的使用this指针变量的、
//所有访问类成员变量的地方,前面都会加上一个 this-> 、
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
//该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以
//一般情况下不需要手动加上、
//此处访问的是某个对象的_year,_month,_day,并不是类体中的,具体是哪个对象,就看实参传
//的是哪个对象的地址,这是因为类体中的类成员变量只是声明,不是定义(可以理解成声明和定义)、
}
//const放在了this的左边,修饰的是this指针变量,其本身不能进行改变,但是this指针变量所指的内容是可以被改变的、
void Init(Date* const this, int year, int month, int day) //该行代码不可以这样写,原因同上,Date* const this要放在第一个形参位置上、
{
//this = nullptr; //this指针变量本身不能被修改、
cout << this << endl;//该行代码可这样写,不会报错,原因同上、
//所有访问类成员变量的地方,前面都会加上一个 this-> 、
//this指针所指的对象是可以被修改的、
this->_year = year; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
this->_month = month; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
this->_day = day; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
//此处访问的是某个对象的_year,_month,_day,并不是类体中的,具体是哪个对象,就看实参传的是哪个对象的地址,这是因为类体中的类成员变量只是声明,不是定义(可以理解成声明和定义)、
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
//此处调用的类成员函数Init是同一份类成员函数(公共代码区),此处调用的类成员函数Print是同一份类成员函数(公共代码区)、
d1.Init(&d1, 2022, 5, 11); //&d1要放在实参的第一个位置上,该行代码不可以这样写,原因同上、
d2.Init(&d2, 2022, 5, 12); //&d2要放在实参的第一个位置上,该行代码不可以这样写,原因同上、
d1.Print(&d1); //2022-5-11 ,该行代码不可以这样写,原因同上、
d2.Print(&d2); //2022-5-12 ,该行代码不可以这样写,原因同上、
//此处的 &d1 和 &d2 都属于有关 this 指针的内容,在实参部分都不能 显式 的写出、
return 0;
}
8.2、this 指针的特性
1、this 指针的类型:类类型* const 、
2、
只能
在 "非静态类成员函数" 的函数体内部显式的使用 this 指针变量,前提必须把非静态类成员函数的声明和定义(可以理解成声明和定义)都放在类体里面,由于静态类成员函数和全局函数等其他类型的函数内部都没有隐藏的 this 指针,所以在全局函数和静态类成员函数等其他类型的函数的函数体内部并不能使用显示的 this 指针、
3、
this 指针本质上其实是一个非静态类成员函数的形参,是对象调用非静态类成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针,在计算对象所占内存空间的大小时,并没有把 this 指针所占的内存空间算入对象所占内存空间中,也能表明对象中不存储 this 指针、
4、
this 指针是非静态类成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递,如果用户强制传递,那么编译器会报错、
5、
形参指针变量 this 是编译器默认加上的,无论非静态类成员函数的函数体内是否用得到,编译器都会默认加上,即使该非静态类成员函数的函数体内为空,编译器也会默认加上、
6、
this 指针变量在非静态类成员函数开始执行前构造,在非静态类成员函数执行结束后清除、
7、
const 修饰 this 指针变量,所以 this 指针变量本身并不能被修改,故为常量(常属性),若在非静态类成员函数的函数体内进行显式的 &this 操作是不对的,因为,不能对常量(常属性)进行取地址操作、
8、
this 指针变量只有在非静态类成员函数的函数体中才有声明和定义,且存储位置会因编译器不同有不同存储位置、
9、
类的 6 个默认的类成员函数中均包含有 this 指针,这 6 个默认的类成员函数也都属于非静态类成员函数、
8.3、关于 this 指针常见的面试题
1、问:this 指针是存在哪的?
答:一般情况下(大部分编译器下)是存在于 栈区 上的,因为 this 指针是形参,当然,有的编译器会使用寄存器进行优化(比如 VS 编译器就会使用寄存器进行优化,并且在 VS 编译器下,在调用函数建立栈帧之前先要进行压参数,该编译器是从右往左压参数的,其他编译器并不一定是从右往左,并没有进行明确的规定)(为什么进行优化?因为我们可能在非静态类成员函数中频繁的使用 this 指针,若存在栈区上,即存在内存中,其在内存中的读取速度比较慢,频繁的使用 this 指针就会导致整体效率较低,所以要使用寄存器进行优化,是因为寄存器的读取速度更快,具体在三级缓存中有讲解,所以当频繁使用 this 指针时,进行寄存器优化会提高效率),将 this 指针存放在 ecx 寄存器上,也就是说,this 指针在某些编译器下可能会存在于 ecx 寄存器中,比如 VS 编译器、
2、问:this 指针可以为空吗?
答:可以,因为无论是传递什么,传递过去的都是一个值,空指针也只是一个值,如何理解空指针? 以32位为例,在进程地址空间中,空指针是一个存在的地址,若是32位的话,内存区域总共占4G,是规定的,也和指针的大小具有关联,从0x00000000到0xFFFFFFFF,总共能够存储2^32次方个地址,每个地址存储单位是byte,则总共能够存储2^32byte个地址,即有这些个编号,1个byte等于8bit,所谓,指针就是地址,地址就是指针,地址是一个编号,所以指针也是编号,所以空指针即为0x00000000,第 0 byte的编号,所以空指针是一个存在且有效的地址,而对空指针进行解引用操作报错的原因是因为:空指针所在的位置是预留出来的,不存储任何数据,不能进行初始化,所以不能够去空指针所指向的内存空间中访问数据,因为他其中是不存储任何数据的,系统会对这一操作进行检查, 若检查到对空指针进行解引用操作,即访问空指针所指的内存空间中的数据时会报错,这一报错是检查规定的行为,内核是存在于高地址上的,还要知道,虚拟内存不是电脑上的物理内存,虚拟内存和物理内存之间还需要通过页表去进行映射,空指针一定是0X00000000,但不是物理内存中的 0 处的地址,而是虚拟内存中 0 处的地址,物理内存可以给任何程序进行映射,不需要再进行划分具体的区域,每个程序中都有物理内存,虚拟内存是每个进程地址空间中都有的,进程地址空间也可以称为虚拟地址空间、
this 指针可以为空,单纯的对 this 赋空是不可以的,不过可以强转直接赋空,不过一般不进行这样的操作、
3、
基类保护成员在子类可以直接被访问,基类私有成员在子类中不能被访问,基类公有成员在子类和类外都可以直接访问,在后期再进行阐述原因、
例题1、
//1.下面程序编译运行结果是?A、编译报错 B、运行崩溃 C、正常运行
#include <iostream>
using namespace std;
class A
{
public:
//编译器处理为:void Show(A* const this)
void Show()//传过来的是一个空指针、
{
//cout << this << endl;
cout << "Show()" << endl;
}
private:
int _a;
//public:
// int _a;
};
int main()
{
A*p=(A*)0x11223344;//程序也能正常运行,打印出Show() 、
A*p = nullptr;
//在非静态类成员函数Show调用的过程中并没有对指针变量p,即空指针指向的那块空间进行访问,而
//是去公共代码区中查找该非静态类成员函数Show的地址,所以不会出现对空指针解引用这样的运行时错误
//,即运行崩溃,并且,不管指针变量p中存储的值是什么,程序都能正常运行,就是因为,会直接去公共代码
//区查找,并不会去指针变量p所指的内存空间中进行访问,即所谓的不进行解引用操作,即使指针变量p中存
//储的值是空指针也是可以的,压根就不会去指针变量p所指的内存空间中进行访问,当然就不会出现运行崩
//溃的错误、
p->Show();//编译器处理为: p->Show(p); 此处的两个 -> 均不是解引用操作,和全局函数的调用是一样的,去公共代码区里查找该非静态类成员函数Show的地址,变成call地址进行调用、
//p->_a = 10; //错误写法,此时->为解引用操作,不可对空指针nullptr进行解引用操作,会报运行时错误、
}
//要知道:访问操作符.和->首次出现在C语言中的结构体章节中,而在C语言中的结构体的定义里面,只
//能进行结构体成员变量的声明,所以通过结构体类型声明和定义出来的结构体变量中把结构体的定义里面
//的东西全部包含了,所以在C语言中的访问操作符.和->代表的意思均为解引用,故,若在C语言中使用到访
//问操作符.和->的话,一律把他们看成解引用操作,而在C++中,已经不存在结构体的概念了,而是把结构体
//升级成了类,而在类的定义中(类体中),不仅仅能声明类成员变量(静态和非静态),还能声明和定义类成
//员函数(静态和非静态),而通过类名实例化出的对象中只包含了类成员变量(静态和非静态),此时,访问
//操作符.和->并不全是代表解引用操作,若访问操作符访问的内容是类成员变量(静态和非静态),则访问
//操作符代表的是解引用操作,这是因为,对象只包含了类成员变量(静态和非静态),若访问的内容是类成员
//函数(静态和非静态)而不是类成员变量(静态和非静态)的话,那么访问操作符代表的就不再是解引用操
//作了,这是因为,对象中并没有包含类成员函数(静态和非静态),所以若在C++中使用到了访问操作符.和->
//的话,首先要观察访问操作符访问的内容到底是类成员变量(静态和非静态)还是类成员函数(静态和非
//静态),访问操作符访问的内容若包含在了对象中,即访问的内容是类成员变量(静态和非静态)的话,那么
//访问操作符代表的意思就是解引用,访问操作符访问的内容若不包含在了对象中,即访问的内容是类成员
//函数(静态和非静态)的话,那么访问操作符代表的意思就不是解引用操作、
//此时,指针变量p中存储的是空指针nullptr,当进行p->Show();时,看似好像在进行解引用操作,但
//是由于指针变量p中存储的是空指针nullptr,所以对空指针nullptr进行解引用操作是不对的,会报错,但
//是这种错误不会是编译错误,即,对空指针nullptr进行解引用或者是对野指针进行解引用操作的话,都属
//于运行时错误,所以一定不会选择A,而是从B和C中选取,而该题的正确答案应该是C,正常运行原因是因为
//,对象里面只包含类体中的类成员变量(静态和非静态),而不包含类成员函数(静态和非静态),所以,题目
//中的->并不是在进行解引用操作,并且,每个非静态类成员函数中都内含了一个this指针,所以在进行p
//->Show();时,其实参部分是把指针变量p传参过去,即把一个空指针nullptr传参了过去,这里虽然const
//修饰的是this指针变量本身,其本身不能被修改,但是空指针nullptr是实参传参过去的,并不受const的
//控制,也就是所谓的,this指针可以为空指针nullptr,即,const修饰的指针变量this本身不能被修改但
//是可以被初始化,而对象中包含类成员变量(静态和非静态),所以若进行p->_a=10;的话,->才起到解引用
//的作用,即对空指针进行了解引用,所以会出现运行时错误、
例题2、
#include <iostream>
using namespace std;
//2.下面程序编译运行结果是?A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
//编译器处理为:
//void PrintA(A*const this)
//{
// cout << this->_a << endl;//此处进行操作this->_a,由于对象中包含了类成员变量(静态和非
//静态),所以此处的->为解引用操作,而形参指针变量this接收到的是空指针nullptr,故此处对空指针进
//行了解引用操作,所以会报运行时错误,相当于访问this指针变量所指向的空间中的内容,所以程序会运
//行崩溃、
//}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
//编译器处理为: p->PrintA(p);
//这里不会出现问题,只是将指针变量p中的空指针nullptr当做实参传给形参this指针变量、
//此处的->不是解引用操作,和全局函数的调用是一样的,去公共代码区里查找该非静态类成员函
//数PrintA的地址,变成call地址进行调用、
}
注意:之前所谓的,在类外不能直接访问类体中私有或保护的内容,其实并不是不能访问,而是不能直接(目前所学的,通过类名加作用域限定符或者通过对象的形式进行的访问称为直接访问)访问,可以通过间接的方法去进行访问,具体等后期再进行阐述、