目录
2.4.1 编译器生成的默认构造函数对内置类型的成员属性不做任何处理!
2.4.2 编译器生成的默认构造函数对自定义类型的成员属性会回去调用该自定义类型的默认构造函数!
2.4.3 自己不写构造函数编译器自动生成默认构造函数,写了则不生成!
补充内容:空类
如果一个类中什么都不写,简称空类。
看到空类这个名词后,引发我的好奇,既然是空类,什么都没有!则实例化对象后在栈区所开辟的内存大小就应该等于0。
可现实却很打脸,通过sizeof()操作符计算出的对象大小竟然等于1,接着我又在空类里加了成员函数,可实例化出的对象大小仍等于1。最后我在类里加入成员属性,这时实例化出的对象大小不等于1(大小遵循C语言阶段结构体的内存对齐规则)。大家可以运行下面代码得到验证。
#include<iostream>
using std::cout;
using std::endl;
class A
{
//空类
};
class B
{
public:
void test()
{
cout << "大家好" << endl;
}
};
class C
{
public:
void test()
{
cout << "大家好" << endl;
}
private:
int _a;
double _b;
char _c;
};
int main()
{
A a;
cout << "对象a大小是:" << sizeof(a) << endl;
B b;
cout << "对象b大小是:" << sizeof(b) << endl;
C c;
cout << "对象c大小是:" << sizeof(c) << endl;
return 0;
}
结论:一个类的大小,实际就是成员属性遵循内存对齐规则所得大小,而空类是比较特殊的一种类,编译器规定给空类1个字节来唯一标识实例化出的对象,而成员函数却存放在公共代码区供该类的所用对象共同使用。
一.类的默认成员函数
上面我们已经了解了空类!
空类中真的什么都没有吗?其实不然,我们在类中什么都不写,编译器会帮我们自动生成默认成员函数,分别有:1.编译器自动生成的默认构造函数,2.编译器自动生成的析构函数,3.编译器自动生成的拷贝构造函数,4.编译器自动生成的赋值运算符重载函数,5.编译器自动生成的取地址运算符重载函数, 今天特意为大家准备了构造函数的详解。 冲!!!
二.构造函数
构造函数分别以下面三个点为大家展开讲解。
2.1 构造函数的定义
构造函数是一个特殊的成员函数,在实例化类对象时编译器为我们自动调用,并且对象的整个生命周期只调用一次。
从上面构造函数的定义,再结合数据结构栈的实现,不难发现对栈而言每实例化一个对象,都需要我们手动为其初始化,重要的是!有时候还会忘记初始化步骤,带来不必要的错误!
那我们直接根据构造函数的定义,使用构造函数为实例化出的对象进行初始化,岂不是很香!同时也避免了自己忘记初始化带来的不必要麻烦和错误。
2.2构造函数的特性
构造函数是特殊的成员函数,所以不要以普通的函数去理解构造函数。构造函数虽然叫做构造,但他不创建对象也不开辟内存,而只是用来初始化!
下面我们来看构造函数的特点:
1.构造函数的函数名必须和类名相同 !
2.构造函数没有返回值类型,且不需要写void !
3.构造函数可以有形式参数,可以构成函数重载 !
4.类实例化对象时自动调用该类的特殊成员函数---构造函数 !
提示:构造函数真正牛的地方就是他的自动调用。
举一个Date日期类的例子,帮助我们更好的理解。
class Date
{
public:
//不带参数的构造函数
Date()
{
}
//带参数的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d; //调用不带参数的构造
Date d1(2024, 8, 2);//调用带参数的构造
return 0;
}
看完上面调用带参的构造函数后,有没有想过使用,Date d ();这样的方式去调用无参的构造函数呢?答案是不可以的,当我们以这样的方式进行书写,关联C语言阶段学习的函数声明便可得知这是错误的写法,编译器会认为Date是函数返回值类型,d是函数名,()无形式参数,从而造成错误。
也可将上面的俩个构造函数合二为一,写成一个全缺省的默认构造函数,这样调用更加随心所欲,也减少了代码量。将构造函数写成全缺省是非常棒的选择!注意:把无参的构造函数删掉,不然使用 Date d 调用构造函数会和全缺省的构造函数造成二义性!
class Date
{
public:
Date(int year = 2024, int month = 8, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//传参注意事项:必须从左向右依次显示传参。
Date d;
Date d1(1);
Date d2(1,1);
Date d3(1,1,1);
return 0;
}
传参注意事项:必须从左向右依次显示传参。
2.3 默认构造函数
再给大家说一下默认构造函数有哪些(这样下面遇到就好理解了):
1.自己不提供编译器自动生成的构造函数;称为:默认构造函数!
class A
{
};
2.自己提供构造函数,但没有形式参数;称为:默认构造函数!
class A
{
public:
A()
{
cout << "构造函数" << endl;
}
};
3.自己提供构造函数,有形式参数但必须为全缺省; 称为:默认构造函数!
class A
{
public:
A(int a = 1, int b = 1)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
注意:创建A类对象会调用构造函数,这里会把对象的地址传给构造函数的this指针。
总结:只要是不传参数就可以调用的都叫做默认构造函数(很明显以上都满足),有且仅有一个。
2.4编译器自动生成的默认构造函数的特性
2.4.1 编译器生成的默认构造函数对内置类型的成员属性不做任何处理!
对于自己写的构造函数我们可以清楚知道函数体是如何实现的,但编译器生成的默认构造函数干了什么事情呢?直接实例化对象让其调用编译器生成的默认构造函数看看做了那些事情如下图。

咦!这家伙不干事呀!d对象的成员全都是随机值!那说明编译器生成的默认构造函数对内置类型的成员属性不做任何处理(注意:随着编译器的不同可能会初始化为0,但只是个别情况)
补充1:在C++11发出后可以给类成员属性缺省值,则默认构造函数会将内置类型初始化为缺省值,原理会在下面的初始化列表中讲到。
补充2:指针是内置类型 eg:char* , Date*。
2.4.2 编译器生成的默认构造函数对自定义类型的成员属性会回去调用该自定义类型的默认构造函数!
class A
{
public:
A(int a = 1,int b = 2,int c = 3)
{
cout << "A()" << endl;
_a = a;
_b = b;
_c = c;
}
private:
int _a;
int _b;
int _c;
};
class B
{
private:
A q1;
A q2;
};
int main()
{
B b1;
return 0;
}
下图展示了对以上代码的调试以及运行结果,B类中自定义类型q1,q2分别回去调用默认构造函数初始化自己,进而使得b1完成初始化,可以看到确实编译器生成的默认构造函数对自定义类型的成员属性会回去调用该自定义类型的默认构造函数。

2.4.3 自己不写构造函数编译器自动生成默认构造函数,写了则不生成!
三.初始化列表
前面我们对构造函数有了一定的了解!接着进入初始化列表的学习。
3.1 构造函数和初始化列表的关系
构造函数初始化的方式有俩种:第一种在构造函数的函数体内初始化。第二种在初始化列表中初始化。所以说初始化列表是构造函数的一部分!
3.2 初始化列表的定义
初始化列表的定义:以一个冒号开始,接着就是以逗号分割的成员属性,每个成员属性后面紧跟一个括号,括号里的内容可以是初始值可以是表达式。那么初始值和表达式就是为每个定义的成员属性进行初始化的!下面用代码展示!
class Date
{
public:
Date(int year = 1 , int month = 2, int day = 3)
:_year(year)
, _month(month)
,_day(day)
{
_day = 6;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
return 0;
}
注意:初始化列表可以和函数体混着使用。
3.3初始化列表的特性
首先呢:初始化列表是每个成员属性定义的地方,类实例化对象是成员属性整体定义的地方,下面用图展示标识出红色字体对应的代码!

问题来了!既然都整体定义了,为什么还要使用初始化列表对每个成员属性再单独定义呢!!!原因如下:当封装一个类,对成员属性声明时,可能会使用到一下三种特殊的成员变量:const成员变量,引用成员变量,没有默认构造的自定义类型成员变量。
变量 | 特征 |
const成员变量 | 必须在定义时进行初始化 |
引用成员变量 | 必须在定义时进行初始化 |
没有默认构造的自定义类型成员变量 | 必须在定义时进行相关传参 |
下面代码对三种特殊的成员变量进行验证!
#include<iostream>
using std::cout;
using std::endl;
class A
{
public:
A(int size)
:_size(size)
{
}
private:
int _size;
};
class Date
{
public:
Date(int year , int month, int day, int& j)
:_year(year)
,_day(day)
,_n(10)
,_ref(j)
,_a(1) //对_a进行定义且传入参数,调用非默认构造函数
{
}
void add()
{
_ref++;
_ref++;
_ref++;
}
private:
int _year;
int _month;
int _day;
const int _n;
int& _ref;
A _a;
};
int main()
{
int x = 10;
Date d(2024,8,3,x);
d.add();
cout << x << endl;
return 0;
}
以上代码就是对三个特殊的成员变量相关的解释。下面有意思的来了,给大家分析几个初始化列表有意思的特性!首先呢,将上面代码进行调试,在监视窗口输入this指针来查看相关成员属性的变化的魅力!当执行到Date d(2024,8,3,x);这条语句接着往下调试,发现直接进入了初始化列表由此,得出第一条结论:调用构造函数的时候先执行初始化列表后执行函数体。再接着往下调试到构造函数的函数体前停下!你会发现_month的内容是随机值。如图四。

不由得会问自己这样几个问题:实例化d对象的时候会进行整体定义这时_month就已经是随机值了,而执行完初始化列表依然是随机值,那_month有没有在初始化列表进行定义呢(这时退出调试)!为了得到验证我把初始化列表中的_a(1)删掉,并且将A类的构造函数的参数加上缺省值以形成默认构造函数,在重新进入调试,依然是调试到构造函数的函数体前停下!这时发现自定义类型_a被初始化了!如图五!得出第二条结论:不管在初始化列表写不写,每个成员变量都是要走初始化列表的,只不过在初始化列表阶段不写的话,对于内置类型编译器用随机值初始化,对于自定义类型编译器直接去调用其默认构造函数(这就与我们前面讲的自定义类型成员变量会回去调用他的默认构造函数形成闭环)。

有时候呀!我们书写初始化列表时对每个成员变量的初始化顺序和声明顺序不一致!这时会发生什么情况呢!通过调试!推出第三条结论:成员变量在类中的声明顺序,就是初始化列表的初始化顺序,与初始化列表里的初始化顺序毫不关联。
在最开始聊构造函数的时候提到下面一句话:
补充1:在C++11发出后可以给类成员属性缺省值,则默认构造函数会将内置类型初始化为缺省值。
代码演示:
class A
{
public:
A(int size)
:_size(size)
{
}
private:
int _size;
};
class Date
{
public:
Date(int year, int month, int day, int& j)
:_year(year)
, _day(day)
, _n(10)
, _ref(j)
{
}
private:
int _year = 2023;
int _month = 6;
int _day = 6;
const int _n;
int& _ref;
A _a = 1;
};
int main()
{
int x = 10;
Date d(2024,8,3,x);
return 0;
}
图六对上面代码分析

通过调试可以看到只有_month和_a被缺省值初始化了,其余_year和_day没有被缺省值初始化,得出结论四:C++11发出后可以给类成员属性缺省值,本质是将缺省值给初始化列表,如果初始化列表没有显示给值,那么就按缺省值来初始化,如果给了值就按给的值进行初始化!自此与前面内容形成闭环
拜拜帅哥美女,下一篇文章见!