1.类在内存中的存储
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
class Person { public: Person() { mA = 0; } //非静态成员变量占对象空间 int mA; //静态成员变量不占对象空间 static int mB; //函数也不占对象空间,所有函数共享一个函数实例 void func() { cout << "mA:" << this->mA << endl; } //静态成员函数也不占对象空间 static void sfunc() { } };
此程序的运行结果
所以只有int mA;时占对象的空间大小
为什么呢
1.静态成员变量 静态成员变量是存储在静态区(全局区),而类属于临时变量,存放在栈区
2.函数 类中的方法(即类中定义的函数)不会占用每个类实例的额外内存空间,是因为它们在内存中只存储一份,而不是每个实例都存储一份。它们被所有类的实例共享。这样可以节省内存空间,并且提高了代码的执行效率。
3.静态成员函数 存放在静态区,也是共享。
类的内存大小计算
和struct的大小计算一样,都要进行内存的对齐,
特殊情况:空类的大小是1
内存对齐
下列是结构体内存对齐规则
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
class A
{
private:
char c;
int i;
}
所以上述类的大小是8字节
为什么要进行内存对齐
假设有三种读取方式:
方式一(内存对齐时):按照对齐的方式读取:先读取char,然后丢弃三个二进制位,再直接从int的开始位置进行读取。
方式二(不内存对齐时):先读取char,然后从char的下一个位置,也就是和char连续存储的int的位置。
方式三:先读取char,然后将剩余的三个字节存起来,再读取int的剩下的字节,然后将这两部分拼接起来。
首先,计算机根据地址线的大小,来确定能给多少存储器进行编码,是按照整数倍来读取,而不是从任意位置进行读取。而如果选择读取多次再进行数据的拼接的话更加麻烦,相比之下,内存对齐时只需要按顺序读取再丢掉不需要的那部分即可。
即:内存对齐就是计算机在读取数据的时候,尽量可以一次取到完整的数据而不是要分很多次才能拿到完整的数据,而且分多次拿到的话还需要进行数据的拼接组合,这样就会更加麻烦了。
实际上就是牺牲空间去换取时间效率的过程
2.this指针
2.1引入this指针
第一节我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。
this指针指向被调用的成员函数所属的对象
this指针的本质是一个指针常量,指针的指向不可修改
2.2 this指针用途
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
1.同名
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
age = age;
}
int age;
};
int main() {
Person p1(10);
cout << "p1.age = " << p1.age << endl;
return 0;
}
此时编译器就会默认构造函数中是三个age都为函数的形参,而类的对象没被改变,打印出来是随机数,而在age前面加上this->后,编译器就可以识别等号右边的age是类的对象
2.返回对象本身
class Person
{
public:
Person(int age)
{
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
int main() {
Person p1(10);
cout << "p1.age = " << p1.age << endl;
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;
}
return 0;
}
上述代码的值是几?
是40
当我们调用PersonAddPerson(p1)函数后,首先p2 = 10 +10 = 20,然后函数返回了p2这个类的实例的地址,然后继续进行函数的调用,一共三次,所以加了30,变成40,这就完成了函数的一次链式相加
但是,当我们把函数的返回类型更改后 Person PersonAddPerson(Person p)
结果有是多少呢
20
为什么呢?因为,这次函数返回的是一个person类,返回一个新的类的实例,也就是匿名对象,此时的类和最开始的p2没有任何关系,所以p2相当于仅进行一次相加。
2.3 空指针访问成员函数
当我们将类的指针变成空指针后,会发生什么情况?
//空指针访问成员函数
class Person {
public:
void ShowClassName() {
cout << "我是Person类!" << endl;
}
void ShowPerson() {
cout << mAge << endl;
}
public:
int mAge;
};
int main() {
Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
p->ShowPerson(); //但是如果成员函数中用到了this指针,就不可以了
return 0;
}
1.当我们将p->ShowPerson(); 函数注释掉,发现函数正常运行,没有报错
2.当我们将p->ShowClassName();函数注视掉,函数会崩溃
为什么呢?
首先,有*或者->都不一定是解引用,解引用以后要有实际意义编译器才会解引用。
我们上面的(*p)和->本质是给成员函数传递this指针,确定是哪个对象调用的this指针,this指针作为形参传递过去。
代码一:我们只是将空的this指针传递过去,在函数内部并没有解引用,因此程序正常运行。
代码二:我们将空的this指针传递之后,由于需要打印成员变量,因此必须对于this指针解引用从而找到该变量,进而造成空指针的解引用。
PS:当类的对象在成员函数中出现是,会自动在前面加上this指针,只是编译器帮我们省略了
3.const修饰成员函数和成员对象
2.1 常函数
常函数(const member function)是指在类的成员函数声明中使用
const
关键字修饰的函数。
特点:
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
本质是 const Type* const pointer;
即指向的对象不可以改变,加上指向的对象的值也不可以改变。
常函数的声明语法为在成员函数的参数列表后面添加 const
关键字。例如:
class MyClass {
public:
void myFunc() const; // 常函数声明
};
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
2.2 常对象
常对象是指通过
const
关键字修饰的对象,表示该对象在其生命周期内不可被修改。一旦对象被声明为常对象,其成员变量在该对象上就不能被修改。
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
常对象的声明方式为在对象类型前面加上 const
关键字。例如:
const MyClass obj; // 声明一个常对象
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}
//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = NULL; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc() const {
//mA = 10000;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数
}
int main() {
test01();
system("pause");
return 0;
}
PS :mutable
关键字用于修饰类的成员变量,表示该变量可以在 const
成员函数中被修改。通常情况下,const
成员函数不能修改对象的成员变量,因为它们被视为常量。但如果某个成员变量被声明为 mutable
,则即使在 const
成员函数中也可以修改它。
4.友元函数
引入
生活中你的家有客厅(Public),有你的卧室(Private)
客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去
但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
1.1 全局函数做友元
class Building
{
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building);
public:
Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
};
void goodGay(Building * building)
{
cout << "好基友正在访问: " << building->m_SittingRoom << endl;
cout << "好基友正在访问: " << building->m_BedRoom << endl;
}
void test01()
{
Building b;
goodGay(&b);
}
int main(){
test01();
system("pause");
return 0;
}
2.2 类做友元
class Building;
class goodGay
{
public:
goodGay();
void visit();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{
building = new Building;
}
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void test01()
{
goodGay gg;
gg.visit();
}
int main(){
test01();
system("pause");
return 0;
}
2.3 成员函数做友元
class Building;
class goodGay
{
public:
goodGay();
void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
void visit2();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{
building = new Building;
}
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void goodGay::visit2()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
//cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void test01()
{
goodGay gg;
gg.visit();
}
int main(){
test01();
system("pause");
return 0;
}
其实三种类型做友元差不多,理解了其中一项,另外两项就理解了。
2.4 注意事项
1.友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
2.友元关系不能继承
3.互为友元类,则需要
(1)声明和定义分离
(2)一个类在另外一个类之前提前声明
(3)在声明之后,定义之前,类为不完整类型,只能用于指向该类型的指针或引用或者用于声明(不是定义)使用该类型做为形参类型或者返回类型的函数。