《C++ Primer Plus》读书笔记 第12章 类和动态内存分配

第12章 类和动态内存分配

1.特殊成员函数

如果没有定义,C++会自动提供下面这些成员函数:

  • 默认构造函数
  • 默认析构函数
  • 复制构造函数
  • 赋值运算符
  • 地址运算符

2.复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型如下:

Class_name(const Class_name &);

新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:

StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);

每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。

默认的复制构造函数逐个复制非静态成员(成员复制,也称浅复制),复制成员的值。

如果类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题

如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是复制指针,这被称为深度复制。而浅复制只是复制指针值,而不会深入挖掘以复制指针引用的结构。

3.赋值运算符

C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:

Class_name & operator=(const Class_name &);

将已有的对象赋给另一个对象时,将使用重载的复制运算符:

StringBad haedline1(“Celery Stalks at Midnight”);
String knot;
knot = headline1;

对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。赋值运算符的定义应遵循以下原则:

  • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete或delete[]来释放这些数据
  • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。这可以通过检查赋值运算符右边的地址与接受对象的地址是否相同来完成。
  • 函数返回一个指向调用对象的引用。通过返回一个对象,函数可以像常规赋值操作那样,连续进行赋值。

4.重载[]运算符

对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。

假设city是一个用户定义的String类的对象,在表达式city[0]中,city是第一个操作数,[]是运算符,0是第二个操作数。下面是该方法的简单实现:

char & String::operator[](int i)
{
	return str[i];
}

5.静态类成员函数

可以将类成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static)。

不能通过对象调用静态成员函数;实际上,静态成员函数不能使用this指针。如果静态成员函数是在共有部分声明的,则可以使用类名和作用域解析运算符来调用它。

由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。

6.在构造函数中使用new时的注意事项

总的来说,再用new初始化对象的指针成员时必须要特别小心。具体地说,应该注意下面这些事项:

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
  • new和delete必须相互兼容。new应对应于delete,new[]应对应于delete[]。
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论是否带中括号)可以用于空指针。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象

7.逐成员复制

在类Magazine中,类成员的类型为String类或标准string类:

Class Magazine
{
private:
	String title;
	string publisher;
	//...
};

String和string都使用动态内存分配,这是否意味这要为Magazine类编写复制构造函数和赋值运算符?不一定。逐成员复制具有一定的智能。在将一个Magazine对象复制或赋值给另一个Magazine对象时,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。

8.返回对象的注意事项

如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不存在。在这种情况下,将使用复制构造函数来生成返回的对象。

如果方法或函数要返回一个没有共有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用

如果有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为引用的效率更高

如果希望方法和函数的返回对象不被修改(或不能作为赋值语句的左半部分,防止发生错误),应返回const对象

9.指向对象的指针

使用对象指针时,应该注意一下几点:

  • 使用常规表示法来声明指向对象的指针:
String * glamour;
  • 可以将指针初始化为指向已有的对象:
String * first = &sayings[0];
  • 可以使用new来初始化指针,这将创建一个新的对象:
String * favorite = new String(sayings[choice]);
  • 对类使用new将调用相应的类的构造函数来初始化新创建的对象:
String * gleep = new String;
String * glop = new String(“my my my”);
String * favorite = new String(sayings[choice]);
  • 可以使用->运算符通过指针访问类方法:
if(sayings[i].length() < shorest->length())
    //...
  • 可以对对象指针应用接触引用运算符(*)来获得对象:
if(sayings[i] < *first)
	first = &sayings[i];

10.用于对象的定位new运算符

如果在一块内存缓冲区中用定位new运算符创建了对象,当使用delete释放这块缓冲区时不会自动调用这些对象的析构函数。所以要显式地为使用定位new运算符创建的对象调用析构函数。这是需要显式地调用析构函数的少数集中情形之一。

对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。因为晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区

11.构造函数的成员初始化列表

如果Classy是一个类,而mem1、mem2、mem3都是这个类的数据成员,则类构造函数可以使用如下的成员初始化列表语法来初始化数据成员:

Classy::Classy(int n, int m) :mem1(n), mem2(0),mem3(n * m + 2)
{
	//...
}

使用这种语法要注意以下几点:

  • 这种格式只能用于构造函数
  • 必须使用这种格式来初始化非静态const数据成员
  • 必须使用这种格式来初始化引用数据成员
  • 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关

C++11允许使用类内初始化方式进行初始化:

class Classy
{
	int mem1 = 10;
	const int mem2 = 20;
	//...
};

这与在构造函数中使用成员初始化列表等价:

Classy::Classy() : mem1(10), mem2(20) {...}

成员mem1和mem2将分别被初始化为10和20,除非调用使用成员初始化列表的构造函数,在这种情况下,实际列表将覆盖这些默认初始值。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值