1.类对象大小
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date s1;
Date s2;
return 0;
}
类实例化出的每个对象,都有独立的空间,所以对象中肯定包含成员变量,而成员函数是不包含的,首先函数被编译后是一段指令,对象中没有办法存储,这些指令存储在一个单独的区域(代码段),对象要存储的话,只能是成员函数的指针。而对象中是否有存储指针的必要,Date实例化s1和s2俩个对象,s1和s2都有各自独立的成员变量,_year,_month,_day存储各自的数据,但是s1和s2成员函数Init和Print指针却是一样的,如果每个都开一个空间给成员函数就浪费了。函数指针是不需要存储。
内存对齐规则
1.第一个成员在于结构体偏移量为0的地址处
2.其它成员要对齐到某个数字(对齐数)的整数倍的地址处
3.对齐数是编译器默认的一个对齐数于该成员大小的较小值(vs默认对齐数为8)
4.结构体总大小:最大对齐数(所有变量类型最大者与默认对齐数取最小)的整数倍
5.嵌套结构体的话,先算出里面结构体的大小,里面结构体的对齐数就是它内部成员对齐数最大的哪一个,最后结构体的整体大小就是所有最大对齐数的整数倍。
例子:
class T
{
char _ch;
int i;
};
类的成员有char型的_ch和int型的i,第一个是char则在偏移量为0的地址处,大小为1,int对齐数为4,要对齐到偏移量为4倍数的位置,就要到偏移量为4位置, 此时占了八个字节,类的最大小要为最大对齐数的整数倍,成员对齐数最大为int(4),八为4的倍数,所以类的大小为八个字节。
下面创建一个没有内容的类A,它的大小为1,因为如果一个字节不给,怎么去表示对象存在过,这里的1字节是为了占位标识对象存在。
class A
{
};
int main()
{
Date s1;
Date s2;
T a;
A b;
cout << sizeof(b) << endl;
return 0;
}
2.this指针
1.Date类中有Init与Print俩个成员函数,函数体中没有关于不同对象区分,当s1调用Init和Print函数时,该函数是如何知道访问的是s1对象还是s2对象呢,那么这里就看C++给了一个隐含的this指针解决问题
2.编译器编译后,类的成员函数默认在形参第一个位置,增加一个当前类型的指针,而这个指针this有指向这个变量成员的访问权限,那么就可以知道是哪一个对象去执行函数
void Init(Date* const this,int year,int month,int day)
3.类成员函数中访问成员变量都是通过this指针访问的,如给int _year赋值,
this->_year=year;
4.C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会自动处理),但是可以在函数体内显示使用this指针。
5.this指针存储在栈里面(寄存器)
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date s1;
Date s2;
s1.Init(11, 22, 33);
s1.Print();
return 0;
}
下面的代码,创建类的指针p指向nullptr,然后通过->去找到成员函数,需要注意的是空指针是不能解引的,但是这里->符号代表的不是解引的意思,它还有别的作用,这里是关于汇编的一些指令
,并不是解引,当p->_a=1就会出错了,因为这时->就是解引的意思了
#include<iostream>
using namespace std;
class a
{
public:
void print()
{
cout << this << endl;
cout << "a::print()" << endl;
}
private:
int _a;
};
int main()
{
a* p = nullptr;
// mov ecx p
p->print(); // call 地址
//p->_a = 1;
}
下面的代码会出错,是因为Print里面有成员变量,但是类p是指向nullptr,所以会有空指针解引。
cout<<this->_a<<endl;
class a
{
public:
void print()
{
cout << "a::print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
a* p = nullptr;
// mov ecx p
p->print(); // call 地址
//p->_a = 1;
}
3.类的成员默认函数
默认成员函数就是没有显示实现,编译器会自动生成的成员函数称为默认成员函数。一个类,在我们不写的情况下编译器会默认生成一下六个默认成员函数
3.1构造函数
构造函数是特殊的成员函数,构造函数的主要任务不是开辟空间创建对象,而是对象实例化时初始化对象。构造函数本质是代替以前Stack中Init函数的功能,构造函数自动调用的特点就很好的代替了Init。
构造函数的特点:
1.函数名字与类的名字一样
2.无返回值。(函数名字前面的类型也不用写,C++规定)
3.对象实例化时系统会自动调用对应的构造函数
4.构造函数可以重载
5.如果没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦有显式定义编译器就不会自动生成
6.无参数构造函数,全缺省构造函数,不写显式构造函数编译器默认生成的构造函数,都叫为默认构造函数。但是三个有且只有一个能存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。不是编译器默认生成的叫默认构造,实际上无参构造和全缺省构造函数也是默认构造。
7.编译器默认生成的构造,对内置类型成员变量的初始化没有要求,初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果成员变量没有默认构造函数就会报错。(就是创建了一个有参数的构造函数,但是仅有这一个,又没有初始化自定义类型变量就会报错)
#include<iostream>
using namespace std;
class date
{
public:
//1.无参构造函数
//date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
//
2.带参构造函数
//date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
// 3.全缺省构造函数
/*date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1;
d1.print();
/*date d3(2024);
d3.print();*/
//date func();
//func.print();
return 0;
}
编译器会自动调用MyQueue的默认构造函数,这个构造函数又会去调用其的成员Stack的默认构造函数
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
// 编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
// 编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
// 显示写析构,也会自动调用Stack的析构
~MyQueue()
{
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;
Stack popst;
//int size;
};
int main()
{
MyQueue mq;
//Stack st1;
//Stack st2;
return 0;
}
补充:
对于成员变量的初始化,C++中确实有一些规则和行为:
1.内置类型成员变量的初始化:
2.如果你没有显式初始化内置类型的成员变量,在编译器默认生成的构造函数中,它们的值是未定义的(即可能是随机值或者未初始化的状态)。编译器不会自动为内置类型成员变量进行初始化。
3.自定义类型成员变量的初始化:
4.当你有一个自定义类型的成员变量时,编译器会尝试调用该类型的默认构造函数来初始化这个成员变量。如果该自定义类型没有默认构造函数(即没有参数的构造函数),并且你没有显式提供初始化方式,则会导致编译错误。因此,如果你自定义了一个类并将其作为成员变量,但没有默认构造函数,你必须提供初始化方法,否则编译器无法生成默认构造函数。下面是一个示例来说明这些行为:
#include <iostream>// 自定义类没有默认构造函数
class NoDefaultConstructor {
public:
NoDefaultConstructor(int value) {
std::cout << "NoDefaultConstructor constructor called with value: " << value << "\n";
}
};// 包含内置类型和自定义类型成员变量的类
class MyClass {
private:
int integer; // 内置类型成员变量
NoDefaultConstructor custom; // 自定义类型成员变量public:
MyClass() {
// 如果没有显式初始化 integer,其值是未定义的
std::cout << "MyClass default constructor called\n";
}
};int main() {
MyClass obj; // 创建 MyClass 对象
return 0;
}在这个例子中:
5.MyClass 包含了一个 int 类型的成员变量 integer 和一个 NoDefaultConstructor 类型的成员变量 custom。
6.NoDefaultConstructor 没有默认构造函数,因此在 MyClass 的构造函数中必须显式初始化 custom,否则编译器会报错。总结来说,C++中对于自定义类型的成员变量,确实要求调用其默认构造函数进行初始化,否则会导致编译错误。对于内置类型的成员变量,编译器不会自动进行初始化,它们的值是未
3.2析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,就释放了,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类似Destory函数的功能。
析构函数的特点:
1.析构函数是在类名前加上字符~
2.无参数无返回值,函数名字前面不用加类型
3.一个类只能有一个析构函数,若为显式定义,系统会自动生成默认的析构函数
4对象生命周期结束时,系统会自动调用析构函数
5.跟构造函数类似,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数
6.显式写析构函数,对于自定义类型成员也会调用它的析构函数,也就是说自定义类型成员无论什么情况都会自动调用析构函数
7.如果类没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果编译器生成的析构函数可以用也可以不写显式析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
如果有俩个Stack要先析构哪一个呢,跟栈很像后进先出,最后的先析构再到前面的,而类里面包类的则外面的先析构再到成员析构。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
// 编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
// 编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
// 显示写析构,也会自动调用Stack的析构
~MyQueue()
{
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;
Stack popst;
//int size;
};
int main()
{
MyQueue mq;
Stack st1;
Stack st2;
return 0;
}
补充:
在C++中,如果你显式定义了一个类的析构函数,无论这个类是否包含其他自定义类型的成员变量,当对象被销毁时,它的析构函数会被自动调用。这也适用于成员变量的析构函数。让我们通过一个示例来说明这一点:
假设我们有一个自定义的类 Member 和一个包含该类对象的类 Container,并显式定义了析构函数。
#include <iostream>
class Member {
public:
Member() {
std::cout << "Member constructor called\n";
}
~Member() {
std::cout << "Member destructor called\n";
}
};
class Container {
private:
Member member;
public:
Container() {
std::cout << "Container constructor called\n";
}
~Container() {
std::cout << "Container destructor called\n";
}
};
int main() {
Container c; // 创建一个 Container 对象
return 0;
}
在这个例子中:
1.类 Member 有一个构造函数和一个析构函数,它们会在对象创建和销毁时输出相应的消息。
2.类 Container 包含一个 Member 类型的成员变量 member。
3.在 main 函数中,创建一个 Container 对象 c。
当程序运行时,输出将会是:
Member constructor called
Container constructor called
Container destructor called
Member destructor called
这表明,在 Container 对象 c 被销毁时,首先调用 Container 的析构函数,然后调用 Container 的成员 Member 的析构函数。这是因为当一个对象被销毁时,它的成员对象也会相应地被销毁,即调用其析构函数。
因此,无论是否显式定义了析构函数,自定义类型的成员变量在对象销毁时都会自动调用它们的析构函数。