C++:构造、析构、引用与拷贝构造

2023-10-31

构造函数


类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

类的数据成员多为私有的,要对它们进初始化,必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次。这个函数就是构造函数。它由系统自动调动,用户不可以调动。

构造函数是类的特殊的公有成员函数,有如下特征:

  • 函数名与类名相同,如:CGoods();

  • 无函数返回类型说明(注意:是没有而不是void,即什么也不写,不可写void。实际上构造函数有返回值,返回的是构造函数所创建的对象)

  • 系统自动调用。在程序运行时,当新的对象被建立,该对象所属的类的构造函数被系统自动调用,在该对象生存期中也只调用这一次,程序员不允许手动调动构造函数,例如a.CGoods("C#",76,78);是不允许的,构造函数它是对象创建时系统自动调用的

  • 可以重载,由不同的参数表区分重载

  • 构造函数可以在类中定义,亦可以在类外定义

    • 在类外定义构造函数时要加作用域限定符,如下:

    • CGoods::CGoods(char *name, int amount, float price)
      {
          strcpy(Name,name);
          Amount = amount;
          Price = price;
          Total_value = price * amount;
      }
      
  • 若类中没有给出构造函数,则C++编译器自动给出一个缺省的构造函数: 类名(void){},只要我们定义了一个构造函数,系统就不会自动生成缺省的构造函数;如果对象的数据成员全为公有的,也可以在对象名后加={},在花括号中顺序填入全体数据成员的初始值

注意:构造函数是在已经分配好的空间上初始化对象成员。给对象分配空间是在进入主函数时系统就分配了(静态创建的对象)或者new的时候运行时分配的(动态创建的对象)

补充:空间与对象

不同于C面向过程中内置类型的有空间即可操纵,面向对象中空间与对象是分离的,有空间不一定有对象,面向对象中有空间了,对象被构造函数初始化之后才可操纵。空间要回收的时候,必须先执行析构函数,再回收空间。Java有垃圾回收机制,程序员只需要关注对象就可以了,而C++里程序员既需要关注对象,还要关注空间。

扩充:C++类的默认函数

对于任何一个类,即便它没有任何函数,编译器将会自动地给它添加6个缺省函数,分别是构造函数、拷贝构造函数、重载=的函数、析构函数、重载&的函数(2个) (如果程序员显示地给出,编译器就不会添加),如下Empty类的6个默认函数:

class Empty
{
public:
	Empty() {}  //构造函数
    Empty(const Empty &e) {}  //拷贝构造函数
    Empty & operator=(const Empty &e)  //重载=的函数
 	{
    	return *this;
	}
    ~Empty() {}  //析构函数
 
 	Empty *operator&() { return this; }  //重载&的函数
 	const Empty * operator&() const { return this; }  //重载&的函数
}

构造函数的使用

创建对象:

CGoods Car1("Ford",30,9999);等效于 CGoods Car1 = CGoods("Ford",30,9999)

若不带参数:

CGoods Car1;

注意:定义不带参对象时,不能加括号,例如:CGoods Car1(),它定义的是一个返回值类型是CGoods类型的无参函数

析构函数


”生而不同,死而相同“:对象可由不同的重载构造函数来初始化,但是销毁的时候都用相同的析构函数

当一个对象被定义时,C++自动调用构造函数对该对象进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数进行扫尾工作,这个特殊的成员函数就是析构函数。

析构函数有如下特点:

  • 析构函数名与类名相同,但在前面加上取反符号’~’,如:~CGoods()
  • 析构函数无函数返回类型
  • 析构函数不带任何参数
  • 一个类有一个也只有一个析构函数,这与构造函数不同
  • 析构函数可以缺省
  • 对象注销时,系统自动调用析构函数,但是程序员也可以在对象生存期没到时,手动调动析构函数,完成对象的自杀动作,如a.~Object();是可以的

注意:析构函数本身并不释放对象占用的内存空间,它只是在系统收回对象的内存空间之前执行扫尾工作

构造与析构顺序

对于如下代码

class Object
{
	int value;
public:
	Object(int x = 0) :value(x)
	{
		cout << "Create Object: " << value << endl;
	}
	~Object()
	{
		cout << "Destory Object: " << value << endl;
	}
};

void fun()
{
	Object a(1);
}

Object b(2);

int main()
{
	Object c(3);
	fun();
	return 0;
}

Object d(4);

执行结果为:

在这里插入图片描述

分析:

当程序编译链接通过时,形成可执行文件,运行可执行文件时,系统给该进程分配了四个空间:代码区.text、数据区.data、堆区.heap、栈区.stack
在这里插入图片描述

程序主函数执行时,先处理全局变量,放入.data区,所以先创建b和d;然后主函数接着执行,执行object c(3),创建c;再执行fun(),创建a,退出fun函数时,收回fun函数栈帧,所以局部变量对象a被销毁,对象a的析构函数被执行;再回到主函数接着执行return,主函数结束,主函数栈帧回收,主函数里的对象c被销毁;接着程序结束,给该进程分配的用户空间要回收,全局对象被依次销毁,所以打印出来的顺序就是2 4 3 1 1 3 4 2

构造函数与析构函数的this指针

构造函数和析构函数也有this指针

Object c(3);

其实就是

Object(&c,3);	

构造的时候把c这个对象传给this指针,构造函数就知道它要操纵的具体是哪个对象了

C++中除了静态函数和友元函数,其他所有类的成员函数都含有this指针

更多关于this指针的介绍,请参考我的另一篇博客:C++:类、对象、this指针、内联函数

用new创建对象

new是C++关键字中比较特殊的一个,它是一种运算符

Object *op = NULL;
op = new Object(10); 

new做了两个动作

  • 从堆区申请空间
  • 调动构造函数初始化对象

与C中malloc不同,malloc只申请空间

用delete删除对象

delete op;

delete做了两个动作

  • 调动对象的析构函数
  • 释放空间

引用


C++函数中参数的传递方式是传值。在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中。它的优点是有效避免函数的副作用。

如果要求改变实参的值呢?如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢?

在C++中有一种新的导出型数据类型——引用(reference),可以解决上面的难题,引用又称为别名。

引用&

就是别名/外号, 一旦引用初始化为某个变量,就可以使用引用名称直接使用该变量,就是给该变量起了个别名

引用不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是C++系统不为引用类型变量分配内存空间。引用主要用于函数之间的数据传递。

引用的定义格式:类型 & 引用变量名 = 已定义过的变量名

例如:

double number;
double &newnum = number;//newnum是新定义的引用类型变量,newnum也就是number的别名

引用的特点

  1. 没有空引用
  2. 定义引用时必须进行初始化
  3. 没有二级引用,即没有引用的引用

引用的使用

C语言使用指针交换a,b

void Swap_c(int *pa, int *pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

C++使用引用交换a,b

void Swap_cpp(int &a, int &b)//在函数中对a,b的交换,就是对实参的交换
{
	int tmp = a;
	a = b;
	b = tmp;
}

C中使用指针间接交换了a,b;而C++使用引用直接操纵实参进行了交换

补充:&的使用

  1. 位运算与:数字之间:1 & 0 --> 0
  2. 取地址符:变量名前:&a
  3. 引用:类型与标识符中间:int &c = a;

引用的本质

对于如下代码:

void fun(int &ap)
{
    int *p = &ap;
    ap = 100;
}
int main()
{
    int x = 10;
    int &p = x;//p就是x的别名
    p = 200;
    fun(x);
    fun(p);
    return 0;
}

编译的时候,编译器会把引用改写成常指针,上述代码就变成如下代码:

void fun(int * const ap)//const修饰ap指针自身,ap自身不可变,但*ap可变
    					//const修饰直接右边,基本类型对const透明
{
    int *p = ap;
    *ap = 100;
}
int main()
{
    int x = 10;
    int * const p = &x;
    *p = 200;
    fun(&x);
    fun(p);
    return 0;
}

在这里插入图片描述

从逻辑角度看,引用就是别名,指针是地址;从底层来看,引用就是常性的指针

注意:函数内局部变量的地址不能作为返回值

对于如下代码:编译的时候都会报警告

int *funp()
{
	int a = 0;
	return &a;
}
int & funy()
{
	int x = 100;
	return x;
}
int * funarr()
{
	int arr[10] = { 0 };
	arr[0] = 124;
	return arr;
}

在这里插入图片描述

函数执行完毕后函数栈就被释放了,不能再对函数内局部变量的地址进行操作,因而不能返回局部变量的地址,可以返回局部变量的值,函数返回局部变量值时实际上是返回变量值的拷贝。作为局部变量,在栈区存储,虽然在函数调用结束后所在内存会被释放回收掉,但返回值会有一个拷贝副本,供调用者使用

变量的生存期不受函数生存期影响,才能返回其地址,如动态创建的(堆区)、全局的、静态的

关于函数返回局部变量的更多介绍

拷贝构造函数


拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。拷贝构造函数的功能是用一个已经存在的对象的数据成员去初构造同类型的新对象的数据成员

同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的。在建立对象时可用同一类的另一个对象作为参数来构造该对象,这时所用的构造函数称为拷贝构造函数

对于CGoods类,缺省拷贝构造函数形式如下:

CGoods(const CGoods & cgd) 
{
    Strcpy(Name,cgd.Name);
    Price = cgd.Price;
    Amount = cgd.Amount;
    Total_value = cgd.Total_value;
}

使用拷贝构造:

int main()
{
    CGoods x("C++", 22, 97);
    
    CGoods a(x);//用x的数据成员去初始化新对象a的数据成员
}

注意:拷贝构造函数不是把一个对象赋值给一个空间,面向对象思维里对象是对象,空间是空间,拷贝构造函数仍然是构造函数,只不过参数是同类型的另一个对象

拷贝构造函数的应用场景

1.当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新对象中

如下代码:fun函数的形参是类的对象,在car所指定的空间中构造一个对象时系统就要用到拷贝构造函数

CGoods fun(CGoods car)//函数的形参是类的对象,在car所指定的空间中构造一个对象
{
    CGoods("C#",12);
    return c1;
}
int main()
{
    CGoods x("C++",22,97);
    CGoods a;
    a = fun(x);
}

2.当函数的返回值是类的对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象,再返回调用者

如上代码中,fun函数的返回值是类的对象,在主函数中用a来接收fun函数的返回值,系统不能把c1直接赋值给a,因为局部对象是在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存期只在函数调用处的表达式中并且该临时对象是只读的。即return对象时,实际上是调用拷贝构造函数把该对象的值拷入临时对象,主函数调用者使用的是该临时对象。如果返回的是局部变量,处理过程类似,只是不调用构造函数,只创建一个临时变量副本。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++:构造、析构、引用与拷贝构造 的相关文章