C++ Primer Plus读书笔记
1.类静态成员
类中可以有一些静态变量,不管这个类创建了多少个对象,这个变量始终都是保持不变的,因为它是静态变量,这就给一些在所有对象中都有相同值的类提供了方便。
在进行类的变量定义时,将数组定义成指针的形式,可以不在定义时就将大小分配,而是在构造函数中使用new对其分配空间。
2.析构函数的调用
- 析构函数的调用时机:
- 当在成员函数中创建对象时,该函数执行完毕之后会调用析构函数!
- 当对象是动态变量时,执行完定义该对象的程序块时,将调用该对象的析构函数。
- 当对象是静态变量时,程序结束时将调用对象的析构函数。
- 当对象是用new创建的时,仅当显示使用delete删除对象时,析构函数才会被调用。
3.特殊成员函数
-
C++自动提供的成员函数:
- 默认构造函数
- 默认的构造函数里面是没有内容的
- 默认析构函数
- 类似与默认构造函数,是没有内容的
-
复制构造函数
-
用于将一个对象复制到新创建的对象中,它用于初始化过程中,而不是常规的赋值过程中。它的函数原型为:
class_name(const class_name &) //ZhuMeng(const ZhuMeng &)
新建一个对象并将其初始化为现有对象时,复制构造函数将被调用
默认的复制构造函数复制的是成员的值。
-
-
赋值运算符
- 这种运算符的原型为:
class_name & class_name::operator=(const class_name &);
- 这种运算符的原型为:
- 地址运算符
- 默认构造函数
4.这里介绍一种错误
- 头文件
#ifndef ZHUMENG_H_
#define ZHUMENG_H
#include<iostream>
class ZhuMeng
{
private:
int shi;
int fen;
int miao;
static int numm;
public:
ZhuMeng();
ZhuMeng(int , int , int );
explicit ZhuMeng(int );
~ZhuMeng();
ZhuMeng operator+(ZhuMeng &) const;
friend std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp);
};
#endif
- 实现文件
#include"ZhuMeng.h"
int ZhuMeng::numm = 0;
ZhuMeng::ZhuMeng()
{
shi = 0;
fen = 0;
miao = 0;
numm++;
}
ZhuMeng::ZhuMeng(int shi, int fen, int miao)
{
this->shi = shi;
this->fen = fen;
this->miao = miao;
numm++;
}
ZhuMeng::ZhuMeng(int shit)
{
this->shi = shit;
numm++;
}
ZhuMeng::~ZhuMeng()
{
numm--;
}
ZhuMeng ZhuMeng::operator+(ZhuMeng & aaa) const
{
std::cout << this->shi << std::endl;
ZhuMeng sum;
sum.shi = this->shi + aaa.shi;
sum.fen = this->fen + aaa.fen;
sum.miao = this->miao + aaa.miao;
return sum;
}
std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp)
{
o<<outp.shi<<std::endl<<outp.fen<<std::endl<<outp.miao<<std::endl;
o<<"left num: "<<outp.numm;
return o;
}
- 执行文件
#include<iostream>
#include"ZhuMeng.h"
using namespace std;
ZhuMeng operator*(int all, ZhuMeng all_2)
{
cout<<"success"<<endl;
}
void func(ZhuMeng a)
{
cout<<a<<endl;
}
int main()
{
ZhuMeng default_zhumeng;
ZhuMeng a1(13, 24, 10);
ZhuMeng a2;//=a1; **** #1
func(default_zhumeng);
func(a1);
func(default_zhumeng);
func(a1);
func(a1);
return 0;
}
输出的结果为:
0
0
0
left num: 2
13
24
10
left num: 1
0
0
0
left num: 0
13
24
10
left num: -1
13
24
10
left num: -2
-
下面从以下几个角度来分析
这里的numm在类中作为静态变量,初始值定义在cpp文件中,静态变量在内存中一直存在,作为某个类所有对象之间共享的一个变量。
-
numm的值为什么会变成负数呢?
在重载<<运算符的时候,将一个ZhuMeng的对象作为对象传给这个函数
-
函数的参数传递方式是按值传递,因此编译器内部是将实参的值赋给形参,也就是用到了复制构造函数,这时候会调用实参的析构函数,以及赋值所用到的复制构造函数,并不是使用构造函数,而在C++中默认的赋值运算符是没有让numm++的,因此numm的值会-1。因此想要正确地执行所想的功能,这里应该是定义一个复制构造函数
ZhuMeng(const ZhuMeng & s) { numm++; }
-
1部分的代码有什么问题呢?
这部分代码,把等号加上,就是进行了赋值操作,而默认的赋值函数没有记性numm++操作,同时这个赋值也是不安全的,如果有使用new delete而创建删除的变量,则很有可能会出现错误,因为指针和内存区域也进行了复制,而每次把对象当做实参进行传递之后会调用析构函数,会将前面使用new的变量删除,同时后面如果使用两次的话,会重复删除同一块区域导致意想不到的错误。
-
因此解决方法是显式定义赋值运算符(也就是重载 = 运算符)
ZhuMeng & ZhuMeng::operator=(const ZhuMeng & a) { numm++; //这里重新定义那些使用new分配空间的变量,并将a的对应值赋给他们即可 return *this; }
- 还有一个问题:如果在类的函数中,某些数据是使用new delete函数进行创建的话,则需要在复制构造函数和赋值运算符中创建新的内存区域。如果不这样做,则每一次调用析构函数的时候,删除的都是同一个内容(因为默认的复制和赋值函数都是这样处理的),当一个变量被delete两次的时候,就会发生未知的错误!!切记。
5.构造函数中使用new时应该注意的事项
如果在构造函数中使用new来初始化指针成员,则需要在析构函数中使用delete
new和delete必须相互兼容,new对应delete,new[]对应delete[]
- 如果有多个构造函数,则必须以同样的方式使用new,要么都带[],要么都不带,因为只有一个析构函数,所有的构造函数都必须与他兼容。
- 可以在构造函数中将指针初始化为0(C++11为nullptr),因为delete和delete[]都可以用于空指针。
- 应当定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
- 应当定义一个赋值运算符,通过深度复制的方式将一个对象复制给另一个对象。
- 下面是几个错误的例子
String String()
{
str = "123";//没有使用new
}
String String(const char * s)
{
str = new char;//因为是字符串,没有使用[]是错误的
}
String String(const char * s)
{
len = strlen(s);
str = new char[len+1];//这个是正确的,在长度后面留一个\0的位置
}
5.关于返回对象
-
1 返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率。
-
注意:
- 返回对象将调用复制构造函数,而返回引用则不会
- 引用指向的对象应该在调用函数执行时存在
-
2 返回指向非const对象的引用
-
重载赋值运算符
- 这个是为了提高效率
-
重载<<运算符(与cout一起使用)
- 这个是必须这样做
-
-
3 返回对象
- 如果返回的对象是被调用函数中的局部变量,则不应该按引用的方式返回它,因为在被调用函数执行完毕时,局部对象将执行其析构函数,因此,当控制权回到调用函数时,引用指向的对象将不再存在。
-
4 返回const对象
- 主要是为了防止某些函数的左值被用于计算,比如a+b=c,将c赋给加法的结果,当然这样是没有意义的,如果返回const对象则可能会避免发生这种错误。
总结:如果方法或函数要返回局部对象,则应该返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,则必须返回一个指向这种对象的引用。如果返回引用和返回对象都可以的时候则最好使用返回引用的方法,效率会更高。
6.指向对象的指针
- 使用new获取指向对象的指针,使用delete则会调用对象的析构函数
ZhuMeng * pointer = new ZhuMeng();//括号里针对不同构造函数有不同的写法
- 定位new运算符
char * buffer = new char[512];
ZhuMeng *pc3, *pc4;
pc3 = new (buffer) ZhuMeng();
pc4 = new (buffer + sizeof(ZhuMeng)) ZhuMeng();
使用定位new运算符创建对象时,是使用一个新的对象来覆盖用于第一个对象的内存单元。因此,如上面两行代码所示,必须在原有的地址上有一个偏移量,才能保证两个部分不重叠。如果类动态地为成员分配内存,则会出现错误。
另外一点是使用delete,如果是用定位new运算符创建的对象,则在删除的时候,如果使用了delete pc3,则删掉的是buffer,因为new/delete系统知道已经分配的是512个字节的内存,而对定位new运算符的操作则是一无所知。由于pc3和buffer指向的内容是一样的,因此使用pc3删掉的也是buffer。这样的话,pc3和pc4的析构函数也不会执行,需要显式地调用析构函数。
使用delete [] buffer的时候是将整个buffer删除(对应的new [])而不会为每一pc调用析构函数(pc3和pc4的析构函数都不会被调用)。
pc4->~ZhuMeng();//释放部分的内存
pc3->~ZhuMeng();
delete [] buffer;//将整个buffer都给释放掉
7.类的成员初始化列表
- 成员初始化列表是放在构造函数定义的名称后面,用冒号指出
ZhuMeng::ZhuMeng(int qs):shi(qs)
{
//......
}
成员初始化列表就是上面代码片中的
shi(qs)
,将qs赋给shi。打个比方,如果shi是一个const int型的常量,则只能给它初始化而不能给他赋值,如果将
shi = qs
写在构造函数中,则会报错,因为这是赋值,不是初始化,把这个类成员初始化列表写在构造函数后边,则会在执行到构造函数之前,也就是创建对象的时候进行初始化,而不是对其进行赋值,这样就会有很大的好处。只有构造函数可以使用这种初始化列表的方法,并且对于const类的成员,必须使用这种语法,否则,则必须要在类内进行初始化。另外,被声明为引用的类成员也必须要使用这种语法。因为引用与const类似,都是只能在声明的时候进行初始化。
在类内进行初始化与使用初始化列表进行初始化是等价的,也就是下面这两种描述是相同的。
class ZhuMeng{
const int quan = 20;
}
ZhuMeng::ZhuMeng():quan(20)
{
//...
}