《C++ Primer Plus 第六版》学习笔记:第十二章 类和动态内存分配

1. 静态类成员

class StringBad	//在头文件中定义
{
	static int num_strings;	//静态类成员
}
int StringBad::num_strings = 0;	//在方法文件中定义

注意:

  • 静态类成员有一个特点:无论创建了多少类对象,程序都只创建一个静态类变量副本(相似的,所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,及每种方法只有一个副本);
  • 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,应 在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分;
  • 初始化语句指出了类型,并使用了作用域解析符,但没有使用关键字static;
  • 初始化是在方法文件中,而不是在类声明文件中进行的,以为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
  • 但如果静态成员是const整数类型或者枚举型,则可以在类声明中初始化。

简而言之:静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符指出静态成员所属的类。

2. 复制(拷贝)构造函数

系统提供默认复制构造函数,逐个复制非静态成员(成员复制也成为浅复制),复制的是成员的值。

//显示复制构造函数示例:   
StringBad::StringBad(const StringBad & st)
{
	num_string++	//handle static member update
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);
}

注意:参数必须为引用,否则将会无限次递归调用导致栈溢出。

(1)何时调用复制构造函数

  • 将新对象初始化为一个同类对象;
  • 函数按值传递对象和函数返回对象;
  • 编译器生成临时对象。

(2)为什么需要定义显示复制构造函数

  • 类成员变量中有使用new初始化的指针成员时,默认复制构造函数进行的浅复制只会使复制前后两个指针指向同一个地址;不会深入“挖掘”复制指针引用的结构;
  • 当类成员变量中有在新对象被创建时候发生变化的静态数据成员时,默认复制构造函数不会使它发生相应变化。

因此,需要定义一个显示复制构造函数来进行深度复制(deep copy)。

3. 赋值运算符

系统提供默认赋值运算符进行浅复制,并不是所有问题都应该归咎于默认的复制构造函数。

(1)赋值运算符的功能以及何时调用它

下述代码使用了赋值运算符:

StringBad headline1("Celery");
...
StringBad knot;
knot = headline1;	//使用了赋值运算符
//但要说明的是,对象的复制不一定必须用等号进行复制

有必要对第三行代码进行解释:系统首先调用显示复制构造函数(假设已经定义,进行深度复制)生成headline1的副本,然后运用赋值运算符将副本各成员值赋给knot(注意,这里其实是浅复制),任务完成后副本调用析构函数释放相应空间。

现在,问题出现了:在副本调用析构函数之后,其内部指针成员(new初始化)和申请的内存消失,但是赋值运算符进行浅复制kont的指针仍然指向该内存,当knot进行析构时,系统尝试释放一段已经释放的内存,会导致程序异常终止。

所以,我们需要重新定义赋值运算符:

StringBad & StringBad::operator=(const StringBad & st)
{
	if(this = &st)
		return * this;
	delete [] str;
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);
}

注意:

  • 必须首先使用delete[ ],否则如果重新申请一段空间并让str指向它,原来的内存将被浪费掉。
  • 赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。

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

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
  • new和delete必须相互兼容。delete对应delete,new[ ]对应于delete[ ];
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另个一个构造函数中将指针初始化为空(0或NULL或c++11中的nullptr),这是因为delete无论带不带中括号都可用于空指针。
  • 定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。具体地说,复制构造函数应分配足够的空间来存储赋值的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
//该类方法与下面类似:
StringBad::StringBad(const StringBad & st)
{
	num_string++	//handle static member update
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);
}
  • 定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体地说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,赋值数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
//该类方法与下面类似:
StringBad & StringBad::operator=(const StringBad & st)
{
	if(this = &st)
		return * this;
	delete [] str;
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);
}

5.有关返回对象的说明

(1)返回指向const的引用

这里有三点说明:

  1. 返回对象将调用复制构造函数,而返回引用不会,效率更高。
  2. 引用指向的对象应该在调用函数执行时存在。
  3. 参数都被声明为const引用,因此返回类型必须为const,这样才匹配(捂脸,现在还没搞明白这是什么意思…)

(2)返回指向非const对象的引用

两种常见的返回非const对象情形是:重载赋值运算符以及重载与cout一起使用的<<运算符。前者这样做旨在提高效率,而后者必须这样做。

//operator=()的返回值用于连续赋值:
String S1("good luck");
String s2, s3;
s3 = s2 = s1;

在上述代码中,s2.operator=()的返回值被赋给s3。为此,返回String对象或String对象的引用都是可行的,但返回引用避免调用复制构造函数创建新的对象,可以提高效率。在这个例子中返回类型不是const,因为方法operator=()返回一个指向s2的引用,可以对其修改。

//operator<<()的返回值用于串接输出
String s1("good luck");
cout << s1 << "is coming!";

返回值类型必须是ostream&,而不能仅仅是ostream。如果返回类型ostream,将要求调用它的复制构造函数,而ostream没有公有的复制构造函数。而返回cout的引用不会带来任何问题。

6. 再谈new和delete

如果使用new来为对象分配内存,当使用delete时,这将只释放用于保存指针的空间,并不释放指针指向的内存,而该任务由析构函数完成。
1.静态成员函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值