《C++ Primer Plus》12. 类和动态内存分配
动态内存和类
书上的代码:
// main.h
#ifndef MAIN_H
#define MAIN_H
#include <iostream>
class StringBad {
char * str;
int len;
static int num_strings;
public:
StringBad(const char * s);
StringBad();
~StringBad();
friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};
#endif // MAIN_H
// main.cpp
#include "main.h"
#include <cstring>
int StringBad::num_strings = 0;
StringBad::StringBad(const char* s) {
len = static_cast<int>(std::strlen(s));
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
}
StringBad::StringBad() {
len = 3;
str = new char[3 + 1];
std::strcpy(str, "C++");
num_strings++;
}
StringBad::~StringBad() {
--num_strings;
delete [] str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st) {
os << st.str;
return os;
}
int main() {
StringBad s1("Hello");
StringBad s2;
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
return 0;
}
静态成员变量需要在类声明外进行初始化,因为类的声明描述如何分配内存但是不会分配内存
如果是 static const
,那么只有类型是整型(int
、char
等)或者枚举时才能在类的声明内部进行初始化,因为编译器必须确保它能在编译期计算出来(double
也不行),编译器会做一些优化
上面的代码运行没问题,但是如果我们改一下 main
函数:
// main.cpp
...
int main() {
StringBad s1("Hello");
StringBad s2 = s1; // 1
StringBad s3(s1); // 2
StringBad s4 = StringBad(s1); // 3
StringBad s5;
s5 = s1; // 4
return 0;
}
1、2 和 3 都表示将 s1
拷贝给 s2
(1 不一定,要看编译器的实现),4 表示将 s1
赋值给 s5
这几个语句都有问题,不管运行哪个,运行代码都会出现错误:
Process finished with exit code -1073740940 (0xC0000374)
- 对于拷贝操作,
s1
被赋值给了s2
,而 C++ 仅仅执行了浅拷贝(这是默认的拷贝构造函数),s1
和s2
最后依次被析构,而同一个指针被delete
了两次 - 对于赋值操作,同样地,会析构旧的值,并且拷贝新的值(这是默认的赋值运算符),
s1
指向的指针同样会被析构两次
为了解决这个问题,需要自行定义拷贝构造函数和赋值运算符。
特殊成员函数
C++ 自动提供了下面的成员函数:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 拷贝构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符(
&
),如果没有定义
拷贝构造函数
拷贝构造函数的函数原型是:
ClassName(const ClassName &);
默认的拷贝构造函数会拷贝所有的成员,如果成员是个类,那么执行这个类的拷贝构造。
下面的四种情况会调用拷贝构造:
StringBad s2(s1);
StringBad s3 = s1;
StringBad s4 = StringBad(s1);
StringBad * ps5 = new StringBad(s1);
其中,第三种情况可能会直接拷贝构造,也可能创建一个临时变量,然后赋值给 s4
,这取决于编译器
赋值运算符
赋值运算符的原型是:
ClassName & ClassName::operator=(const ClassName &);
默认的赋值运算符与拷贝构造相似,逐个复制所有的成员,如果成员是个类对象,那么执行这个类的赋值运算符
下面的情况会调用赋值运算符:
StringBad s2;
s2 = s1;
在构造函数中使用 new
的注意事项
new
与delete
搭配,new[]
与delete[]
搭配,不要混用- 对于同一个成员变量,不同的构造函数应该用同一种
new
的方式,因为析构函数只有一个 - 应该定义拷贝构造函数、赋值运算符
关于返回对象的说明
- 返回指向
const
对象的引用:提高效率,不会调用复制构造函数 - 返回指向非
const
对象的引用:提高效率,并且可以更改 - 返回对象:如果返回的是一个局部变量
- 返回
const
对象:防止写出if (s1 + s2 = s3)
这样的代码(原本应该是if (s1 + s2 == s3)
),因为const
对象不能被赋值,如果不是const
对象,那么会将s3
赋值给一个临时变量
定位 new
运算符
可以使用定位 new
运算符来在 buffer 中分配内存:
// main.cpp
...
const int BUF = 512;
int main() {
char* buffer = new char[BUF];
StringBad* p1 = new(buffer) StringBad;
StringBad* p2 = new(buffer + sizeof StringBad) StringBad;
p1->~StringBad();
p2->~StringBad();
return 0;
}
new
后面的括号表示对象的地址
定位 new
运算的本质其实就是把传递给他的地址强制转换成 void *
类型然后返回以便能够赋值给任何指针类型
使用定位 new
运算符时,必须确保析构函数被调用,需要手动调用析构函数,不能直接 delete