以下内容源于C语言中文网的学习与整理,非原创,如有侵权请告知删除。
C++中有一种特殊的成员函数,它的名字和类名相同,可以有形参,但没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。
构造函数在创建对象时自动执行,因此我们可以写一个构造函数,在该函数内部,将构造函数的形参赋值给成员变量,这样一来,在创建对象的同时也对成员变量进行了赋值。构造函数在实际开发中会大量使用,它往往用来做一些初始化工作,例如对成员变量赋值、预先打开文件等,当然你也可以让构造函数什么也不做,或者做一些提示工作。
在博文中,我们通过成员函数 setname()、setage()、setscore() 分别为成员变量 name、age、score 赋值,这样做虽然有效,但显得有点麻烦。有了构造函数,我们就可以简化这项工作,在创建对象的同时为成员变量赋值。
请看下面的示例1代码:
- #include
- using namespace std;
-
- class Student{
- private:
- char *m_name;
- int m_age;
- float m_score;
- public:
- //声明构造函数
- Student(char *name, int age, float score);
- //声明普通成员函数
- void show();
- };
-
- //定义构造函数
- Student::Student(char *name, int age, float score){
- m_name = name;
- m_age = age;
- m_score = score;
- }
- //定义普通成员函数
- void Student::show(){
- cout<
"的年龄是"<",成绩是"< - }
-
- int main(){
- //创建对象时向构造函数传参
- Student stu("小明", 15, 92.5f);
- stu.show();
- //创建对象时向构造函数传参
- Student *pstu = new Student("李华", 16, 96);
- pstu -> show();
-
- return 0;
- }
- 小明的年龄是15,成绩是92.5
- 李华的年龄是16,成绩是96
(1)该例在 Student 类中定义了一个构造函数Student(char *, int, float)
,它的作用是给三个 private 属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由英文小括号( )
包围,和普通的函数调用非常类似。
(2)在栈上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f)
;在堆上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)
。
(3)构造函数必须是 public 属性的,否则创建对象时无法调用。当然,设置为 private、protected 属性也不会报错,但是没有意义。
(4)构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:
- 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
- 函数体中不能有 return 语句。
3、构造函数的重载
和普通成员函数一样,构造函数是允许重载的。
一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数,而且必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。
构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。比如对于上面示例代码,如果写作Student stu
或者new Student
就是错误的,因为类中包含了构造函数,而创建对象时却没有调用。
如下面的示例2代码:
- #include
- using namespace std;
-
- class Student{
- private:
- char *m_name;
- int m_age;
- float m_score;
- public:
- Student();
- Student(char *name, int age, float score);
- void setname(char *name);
- void setage(int age);
- void setscore(float score);
- void show();
- };
-
- Student::Student(){
- m_name = NULL;
- m_age = 0;
- m_score = 0.0;
- }
- Student::Student(char *name, int age, float score){
- m_name = name;
- m_age = age;
- m_score = score;
- }
- void Student::setname(char *name){
- m_name = name;
- }
- void Student::setage(int age){
- m_age = age;
- }
- void Student::setscore(float score){
- m_score = score;
- }
- void Student::show(){
- if(m_name == NULL || m_age <= 0){
- cout<<"成员变量还未初始化"<
- }else{
- cout<
"的年龄是"<",成绩是"< - }
- }
-
- int main(){
- //调用构造函数 Student(char *, int, float)
- Student stu("小明", 15, 92.5f);
- stu.show();
-
- //调用构造函数 Student()
- Student *pstu = new Student();
- pstu -> show();
- pstu -> setname("李华");
- pstu -> setage(16);
- pstu -> setscore(96);
- pstu -> show();
-
- return 0;
- }
- 小明的年龄是15,成绩是92.5
- 成员变量还未初始化
- 李华的年龄是16,成绩是96
构造函数Student(char *, int, float)
为各个成员变量赋值,构造函数Student()
将各个成员变量的值设置为空,它们是重载关系。根据Student()
创建对象时不会赋予成员变量有效值,所以还要调用成员函数 setname()、setage()、setscore() 来给它们重新赋值。
4、默认构造函数
如果用户没有定义构造函数,则编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student 类,默认生成的构造函数如下:
Student(){}
一个类必须要有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。比如 Student 类已经有了一个自定义的构造函数Student(char *, int, float)
,则编译器不会再额外添加构造函数Student()
。
实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。
最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作Student stu()
或Student stu
,在堆上创建对象可以写作Student *pstu = new Student()
或Student *pstu = new Student
,它们都会调用构造函数 Student()。
以前我们就是这样做的,创建对象时都没有写括号,其实是调用了默认的构造函数。
5、初始化列表
构造函数的一项重要功能是对成员变量进行初始化。为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表,它会使得代码更加简洁。
比如下面的示例代码3:
- #include
- using namespace std;
-
- class Student{
- private:
- char *m_name;
- int m_age;
- float m_score;
- public:
- Student(char *name, int age, float score);
- void show();
- };
-
- //采用初始化列表
- Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
- //TODO:
- }
- void Student::show(){
- cout<
"的年龄是"<",成绩是"< - }
-
- int main(){
- Student stu("小明", 15, 92.5f);
- stu.show();
- Student *pstu = new Student("李华", 16, 96);
- pstu -> show();
-
- return 0;
- }
(1)定义构造函数时其函数体为空(可以有其他语句),在函数首部与函数体之间添加冒号:
,后面紧跟m_name(name), m_age(age), m_score(score)
语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;
语句,也是赋值的意思。
(2)使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。
(3)初始化列表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name 使用初始化列表,其他成员变量还是一一赋值:
- Student::Student(char *name, int age, float score): m_name(name){
- m_age = age;
- m_score = score;
- }
(4)成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关,比如下面代码:
- #include
- using namespace std;
-
- class Demo{
- private:
- int m_a;
- int m_b;
- public:
- Demo(int b);
- void show();
- };
-
- Demo::Demo(int b): m_b(b), m_a(m_b){ }
- void Demo::show(){ cout<
", "< -
- int main(){
- Demo obj(100);
- obj.show();
- return 0;
- }
-858993460, 100
在初始化列表中,我们将 m_b 放在了 m_a 的前面,看起来是先给 m_b 赋值,再给 m_a 赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo 类中,我们先声明的 m_a,再声明的 m_b,所以构造函数和下面的代码等价:
- Demo::Demo(int b): m_b(b), m_a(m_b){
- m_a = m_b;
- m_b = b;
- }
给 m_a 赋值时,m_b 还未被初始化,它的值是不确定的(obj 在栈上分配内存,成员变量的初始值是不确定的),所以输出的 m_a 的值是一个奇怪的数字;给 m_a 赋值完成后才给 m_b 赋值,此时 m_b 的值才是 100。
初始化 const 成员变量
构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表。
例如 VS/VC 不支持变长数组(数组长度不能是变量),我们自己定义了一个 VLA 类,用于模拟变长数组,请看下面的代码:
- class VLA{
- private:
- const int m_len;
- int *m_arr;
- public:
- VLA(int len);
- };
-
- //必须使用初始化列表来初始化 m_len
- VLA::VLA(int len): m_len(len){
- m_arr = new int[len];
- }
VLA 类包含了两个成员变量,m_len 和 m_arr 指针。需要注意的是 m_len 加了 const 修饰,只能使用初始化列表的方式赋值,如果写作下面的形式是错误的:
- VLA::VLA(int len){
- m_len = len;
- m_arr = new int[len];
- }
二、析构函数的详解
1、析构函数的定义
析构函数(Destructor)是一种特殊的成员函数,它的名字是在类名前面加一个~
符号,没有返回值,没有参数,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。
2、析构函数的作用
创建对象时系统会自动调用构造函数进行初始化工作,销毁对象时系统也会自动调用析构函数来进行清理工作,例如释放分配的内存、关闭打开的文件等。
比如上节我们定义了一个 VLA 类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。请看下面的完整示例:
- #include
- using namespace std;
-
- class VLA{
- public:
- VLA(int len); //构造函数
- ~VLA(); //析构函数
- public:
- void input(); //从控制台输入数组元素
- void show(); //显示数组元素
- private:
- int *at(int i); //获取第i个元素的指针
- private:
- const int m_len; //数组长度
- int *m_arr; //数组指针
- int *m_p; //指向数组第i个元素的指针
- };
-
- VLA::VLA(int len): m_len(len){ //使用初始化列表来给 m_len 赋值
- if(len > 0){ m_arr = new int[len]; /*分配内存*/ }
- else{ m_arr = NULL; }
- }
- VLA::~VLA(){
- delete[] m_arr; //释放内存
- }
- void VLA::input(){
- for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
- }
- void VLA::show(){
- for(int i=0; m_p=at(i); i++){
- if(i == m_len - 1){ cout<<*at(i)<
- else{ cout<<*at(i)<<", "; }
- }
- }
- int* VLA::at(int i){
- if(!m_arr || i<0 || i>=m_len){ return NULL; }
- else{ return m_arr + i; }
- }
-
- int main(){
- //创建一个有n个元素的数组(对象)
- int n;
- cout<<"Input array length: ";
- cin>>n;
- VLA *parr = new VLA(n);
- //输入数组元素
- cout<<"Input "<
" numbers: "; - parr -> input();
- //输出数组元素
- cout<<"Elements: ";
- parr -> show();
- //删除数组(对象)
- delete parr;//第53行
-
- return 0;
- }
- Input array length: 5
- Input 5 numbers: 11 22 33 44 55 66 #这里输入6个数,但根据代码会被截断
- Elements: 11, 22, 33, 44, 55
(1) ~VLA()
就是 VLA 类的析构函数,根据它函数体的代码,可知它的作用是在删除对象(第 53 行代码)后释放已经分配的内存。函数名是标识符的一种,原则上标识符的命名中不允许出现~
符号,在析构函数的名字中出现的~
可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。
(3)注意:at() 函数只在类的内部使用,所以将它声明为 private 属性;m_len 变量不允许修改,所以用 const 进行了限制,这样就只能使用初始化列表来进行赋值。
(3)C++ 中的 new 和 delete 分别用来分配和释放内存,它们与C语言中 malloc()、free() 最大的一个不同之处在于:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用 new 和 delete。
3、析构函数的执行时机
构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
(1)在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
(2)在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
(3)new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
下面的例子演示了析构函数的执行。
- #include
- #include
- using namespace std;
-
- class Demo{
- public:
- Demo(string s);
- ~Demo();
- private:
- string m_s;
- };
- Demo::Demo(string s): m_s(s){ }
- Demo::~Demo(){ cout<
-
- void func(){
- //局部对象
- Demo obj1("1");
- }
-
- //全局对象
- Demo obj2("2");
-
- int main(){
- //局部对象
- Demo obj3("3");
- //new创建的对象
- Demo *pobj4 = new Demo("4");
- func();
- cout<<"main"<
-
- return 0;
- }
- 1
- main
- 3
- 2
分析可知,func函数执行结束时,会调用(在func这个函数中创建的对象)obj1这个对象的析构函数,因此输出1;
然后main函数中输出代码输出“main”;
接着因为“retunr 0”,main函数执行结束时,会调用(在main这个函数中创建的对象)obj3这个对象的析构函数,因此输出3;
最后整个程序执行结束时,会调用(在所有函数之外创建的对象)obj2这个对象的析构函数,因此输出2。
4、析构函数的其他说明
(1)析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。
(2)如果用户没有定义,编译器会自动生成一个默认的析构函数。
-
相关阅读:
Vue面试题
nodejs开发的小程序 老年人健康预警系统
Dubbo学习
centos7.7 安装glibc的问题
物流快递信息查询管理系统网站(JSP+HTML+MySQL)
【计算机网络】UDP/TCP协议
第六篇:常用Linux命令
数据库索引
回收站永久删除了如何恢复正常?
Go 多版本管理工具
-
原文地址:https://blog.csdn.net/oqqHuTu12345678/article/details/133816031