C++类的构造与析构特点及作用详解
更新时间:2022年10月18日 15:06:21 作者:是星星鸭
本文章将会可能会涉及到汇编的知识,不过没有关系,我会讲的尽量通俗易懂;另外本篇文章开始前,建议了解下什么是函数重载,这个概念很简单的--有相同的函数名,但参数列表不相同的函数,就是函数重载
目录
一、类的构造函数什么是构造函数
和类具有相同名称,并且没有返回值类型的函数,就是类的构造函数
概念模糊、直接举例:
#include#include struct Test { Test() // 和类具有相同的名、并且没有返回值 { } }; int main() { return 0; }
构造函数的特点
直接先来说特点吧,然后论证:
1、构造函数在定义对象的时候被调用
2、构造函数可以进行函数重载,可以有很多个
3、创建对象时默认调用的是无参构造
证明1:
构造函数在定义对象的时候被调用;
论证如下:
#includestruct Test { Test() { printf("你调用了构造函数\n"); // 此处下断点、如果该函数被调用肯定会停下来。 } }; int main() { Test te; printf("接力\n"); return 0; }
我们在Test()构造的输出语句上加断点、当程序调用Test的printf肯定会停下来,这个时候我们转到反汇编,单步步过、直到函数返回之后,就能知到刚刚是在哪里调用的构造函数了
vs2010:F7编译、F5调试、ALT+8反汇编:
F10一直运行到返回:
这里编译器做了优化,可以直接看出来是在Test定义对象的时候调用了构造。
证明2:
构造函数可以进行函数重载,可以有很多个;
论证如下:
#include#include struct Test { Test() { printf("你调用了构造函数\n"); // 此处下断点、如果该函数被调用肯定会停下来。 } Test(int a) { printf("重载%d\n",a); } Test(int a, int b) { printf("重载%d\n",a+b); } }; int main() { Test te; Test te1(1); Test te2(1,1); // 注意、调用有参的构造函数时,需要传递参数 system("pause"); return 0; }
重载了两个,注意:调用有参数的构造时,需要传递参数。
运行可以通过:
证明3:
创建对象时默认调用的是无参构造;
论证如下:
#include#include struct Test { Test(int a) { printf("重载%d\n",a); } Test(int a, int b) { printf("重载%d\n",a+b); } }; int main() { Test te; // 普通定义对象的方式、不带参数 system("pause"); return 0; }
首先我们删除无参构造,看看能否编译通过:
不可以
然后删除有参构造:
#include#include struct Test { Test() { printf("你调用了构造函数\n"); // 此处下断点、如果该函数被调用肯定会停下来。 } }; int main() { Test te; system("pause"); return 0; }
可以
全部都加上:
#include#include struct Test { Test() { printf("你调用了构造函数\n"); // 此处下断点、如果该函数被调用肯定会停下来。 } Test(int a) { printf("重载%d\n",a); } Test(int a, int b) { printf("重载%d\n",a+b); } }; int main() { Test te; system("pause"); return 0; }
运行结果:
这已经证明了,Test te;平常这样定义对象的时候,调用的是无参构造。如果需要调用有参构造,必须传入参数;
这个特点很简单、有参函数肯定要传参嘛,所以定义对象的时候肯定要传入参数啊;
但是这里建议无论什么时候写类,最好还是写上无参构造,哪怕什么都不做也尽量写上,避免不必要的麻烦。
构造函数的作用
一般用于初始化类的成员
如下:
#include#include struct Test { int x; int y; int z; Test() { } Test(int x,int y,int z) // 构造函数初始化对象 { this->x = x; this->y = y; this->z = z; } }; int main() { Test te; Test te1(1,2,3); // 定义对象并调用有参构造进行初始化 printf("%d %d %d\n",te1.x,te1.y,te1.z); // 输出看看是否初始化成功 system("pause"); return 0; }
运行如下:
初始化成功。
二、类的析构函数什么是析构函数
类的构造函数名前加上'~'这个符号,就是类的析构函数
概念模糊、代码如下:
#include#include struct Test { Test() { printf("你调用了一次类的构造函数\n"); } ~Test() { printf("你调用了一次类的析构函数\n"); } }; int main() { Test te; // system("pause"); // 这里就不要让程序停下来了,不然析构不了 return 0; }
~Test(){}就这个样子
析构函数的特点
依然直接先来说特点,然后论证:
1、析构函数不能重载、不能有参数
2、析构函数在变量声明周期结束时被调用
3、析构函数被调用分两种情况:堆栈中定义的对象、全局区中定义的对象
证明1:
析构函数不能重载、不能有参数;
编译不通过。
既然不能有参数,那重载更不可能了
证明成功。
证明2:
析构函数在变量声明周期结束时被调用;
局部变量的生命周期是在一个大括号内,即一个所处块结束。
所以:
#include#include struct Test { Test() { printf("你调用了一次类的构造函数\n"); } ~Test() { printf("你调用了一次类的析构函数\n"); } }; int main() { { Test te; printf("te生命周期即将结束。\n"); } // 析构应该在这里被调用 printf("te生命周期结束。\n"); system("pause"); return 0; }
运行结果如下:
断点
结果
证明成功。
证明3:
析构函数被调用分两种情况:堆栈中定义的对象、全局区中定义的对象;
已知堆栈中定义的对象(局部变量)在块语句结束之后就会被调用,那么带有return的main函数是在返回前调用析构,还是返回后呢?
代码如下:
#include#include struct Test { Test() { printf("你调用了一次类的构造函数\n"); } ~Test() { printf("你调用了一次类的析构函数\n"); // 断点--汇编 } }; int main() { Test te; // system("pause"); // 不要使用pause,不然无法返回 return 0; }
断点-调试-汇编:
可以看到是在函数返回前被调用的。
如果在全局区定义的对象呢?
这个问题很难说,像我一样定义两个断点就行了,如下:
#include#include struct Test { Test() { printf("你调用了一次类的构造函数\n"); } ~Test() // 断点 { printf("你调用了一次类的析构函数\n"); } }; Test te; int main() { // system("pause"); // 不要使用pause,不然无法返回 return 0; // 断点 }
运行:
发现一运行就断在了return,当我们发F10继续运行的时候,并没有直接调用析构,而是到了括号那里:
继续F10:
通过翻译,可以得知这就是进程结束时的一些收尾工作。
继续F10:
现在大概可以总结了,类的对象定义为全局变量时,是在main函数结束之后进程退出之前,调用的析构函数。
小结
当类的对象定义为局部变量时(堆栈),定义这个对象的块作用域结束时就会调用该对象的析构函数,如果在main函数这个块作用域中定义的对象,那么就是在return之前调用析构。
当类的对象定义为全局变量时(全局区),会在main函数return函数返回之后和进程结束之前,调用该对象的析构函数。
析构函数的作用
我们知道了析构函数都是在类的对象生命周期结束时被调用,那么就代表下面不会再使用到这个对象;所以析构函数一般用于一些收尾的工作,以防忘记。
比如当你使用了该对象的成员申请了内存(malloc、new等)、或者open了一些文件,那么可以在析构函数中free delete 或者close。
例如:
#include#include struct Test { int x; char* name; Test() { name = (char*)malloc(sizeof(char)*20); // 构造时动态申请 } ~Test() { if(this->name!=0) // 析构时判断是个否为空,不为空释放 { free(name); name = 0; } } }; int main() { Test te; return 0; }
这里我就不运行了,大家可以自己测试下。
总结构造函数
1、和类具有相同名称,并且没有返回值类型的函数,就是类的构造函数
2、构造函数在定义对象的时候被调用
3、构造函数可以进行函数重载,可以有很多个
4、创建对象时默认调用的是无参构造
析构函数
1、类的构造函数名前加上'~'这个符号,就是类的析构函数
2、析构函数不能重载、不能有参数
3、析构函数在变量声明周期结束时被调用
4、析构函数被调用分两种情况:堆栈中定义的对象、全局区中定义的对象
到此这篇关于C++类的构造与析构特点及作用详解的文章就介绍到这了,更多相关C++类的构造与析构内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
C++ primer类的基础精讲
C++primer类的基础精讲
更新时间:2022年07月01日 10:24:15 作者:扑街男孩
C++类,是指系统在第一次在程序中遇到一个类时为这个类建立它的所有类变量的拷贝-这个类的所有实例共享它的类变量
目录
定义抽象数据类型初探this和
struct Sales_data { string isbn(){return bookNo;} Sales_data & combine(const Sales_data&); double avg_price() const; string bookNo; unsigned units_sold=0; double revenue=0; }; Sales_data total;
引入this
对于isbn成员函数的调用: total.isbn();
当我们调用成员函数时,实则上是在替某个对象调用它。在上面的调用中,当isbn返回bookNo时,实际上隐式地返回total.bookNo.
成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,如果调用total.isbn(),编译器负责把total的地址传递给isbn的隐式形参this,可以等价认为编译器将该调用重写成Sales_data::isbn(&total),调用Sales_data时的isbn成员时传入了total的地址。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看做this的隐式调用,例如,isbn在使用bookNo时,隐式地使用this指向的成员,就如同this->bookNo一样。
构造函数
定义:类通过一个或几个特殊的成员函数来控制其对象的初始化,这些函数叫做构造函数。
无论何时,只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名一样,构造函数没有返回类型,一个类可以拥有多个构造函数,但每个构造函数之间必须在参数数量或参数类型上存在不同。且构造函数不能被声明成const。
当一个类没有定义任何构造函数时,编译器会给类自动添加一个默认构造函数,该构造函数无须任何实参对对象进行初始化。
对前面的Sales_data类进行编写构造函数
struct Sales_data { Sales_data()=default; Sales_data(const string &s):bookNo(s)() Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){} Sales_data(istream &) string isbn() const{return bookNo;} Sales_data &combine(const Sales_data&); double avg_price() const; string bookNo; unsigned units_sold=0; double revenue=0.0; }
(1)=default的含义
如果需要默认构造函数起作用,那么可以在参数列表后面写上=default来要求编译器生成默认构造函数。
(2)构造函数初始值列表
Sales_data(const string &s):bookNo(s)()
Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){}
上面出现了两个新的构造函数的写法,该部分称为构造函数初始值列表。
负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的成员初始值。
当某个数据成员被构造函数初始值列表忽略时,他将以合成默认构造函数相同的方式隐式初始化。所以,第一个构造函数等价于:
Sales_data(const string &s):bookNo(s),units_sold(0),revenue(0)();
访问控制和封装
访问控制符public和private
定义在public说明符之后的成员在整个程序可被访问。
定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。
Sales_data类的新形式
class Sales_data { public: Sales_data()=default; Sales_data(const string &s):bookNo(s)() Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){} Sales_data(istream &) string isbn() const{return bookNo;} Sales_data &combine(const Sales_data&); private: double avg_price() const; string bookNo; unsigned units_sold=0; double revenue=0.0; }
友元
类允许其他类或者函数访问他的非公有成员,方法是令其他类或者函数成为他的友元。如果一个类想把一个函数作为他的友元,只需要增加一个friend关键字开始的函数声明语句即可
class Sales_data { friend Sales_data add(const Sales_data &,const Sales_data&); friend istream &read(istream&,Sales_data&); friend ostream &print(ostream&,const Sales_data&) public: Sales_data()=default; Sales_data(const string &s):bookNo(s)() Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){} Sales_data(istream &) string isbn() const{return bookNo;} Sales_data &combine(const Sales_data&); private: double avg_price() const; string bookNo; unsigned units_sold=0; double revenue=0.0; }
友元的声明只能出现在类定义的内部。友元不是类的成员,也不受他所在区域访问控制级别的约束。
类的其他特性可变数据成员
在一个const成员函数中,若希望修改类的某个数据成员,可以通过在变量的声明中加入mutable关键字实现
class screen{ public: void some_menmber() const; private: mutable size_t access_ctr }; void screen::some_member() const { ++access_ctr }
返回*this的成员函数
inline Screen &Screen::set(char c) { contents[cursor]=c; return *this; } inline Screen &Screen::set(pos r,pos col,char ch) { contents[r*width+col]=ch; return *this; } inline Screen &Screen::move(pos r,pos c) { pos row=r*width; cursor=row+c; return *this; }
move和set一样,返回的值是对对象的引用。
myScreen.move(4,0).set('#');
等同于
myScreen.move(4.0);
myScreen.set('#');
假如我们定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变临时副本,不能改变myScreen的值
友元类
例如,window_mgr类的某些成员需要访问screen类的内部数据,例如window_mgr的clear函数将一个指定的screen类的内容设置为空白。
class Screen{ //window_mgr的成员可以访问Screen类的私有部分 friend class Window_mgr //Screen类剩余部分 }
class Window_mgr{ public: using ScreenIndex=std::vector::size_type void clear(ScreenIndex); private: std::vector screens{Screen(24,80,' ')} }; void Window_mgr::clear(ScreenIndex) { Screen &s=screen[i]; s.contens=string(s.height*swidth,' ') }
构造函数再探构造函数初始值列表
(1)构造函数的初始值有时必不可少
如果成员是const或者是引用的话,必须将其初始化
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; };
成员ci和ri必须被初始化,如果没有为他们提供构造函数初始值的话将引发错误。正确形式应该是: ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){};
如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。
(2)成员初始化顺序
成员初始化的顺序与他们在类定义中的出现顺序一致。一般来说初始化的顺序没什么特别要求,不过如果一个成员是用另一个成员来初始化的,那么着两个成员的初始化顺序就关键了。
例如
class X { int i; int j; public: X(int val):j(val),i(j){} };
而编译器实际上是先初始化i,在初始化j,而初始化i的时候发现j没有值,所以上述构造函数会发生错误。
默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化在以下情况下发生:
(1)当我们在块作用域内不适用任何初始值定义一个非静态变量或数组时。
(2)当一个类本身含有类类型的成员且使用合成的默认构造函数时
(3)当类类型的成员没有在构造函数初始值列表中显示地初始化时
值初始化在以下情况发生:
(1)在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时
(2)当我们不适用初始值定义一个局部静态变量
使用默认构造函数