以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# 类和动态内存分配
# 动态内存和类
# 编写一个简单的 string 类
stringbad.h
#pragma once | |
#include <iostream> | |
#ifndef STRINGBAD_H_ | |
#define STRINGBAD_H_ | |
class StringBad {  | |
private:  | |
char* str;  | |
int len;  | |
static int num_strings;  | |
public:  | |
StringBad(const char*);  | |
StringBad();  | |
~StringBad();  | |
// friend func | |
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);  | |
};  | |
#endif // !STRINGBAD_H_ | 
stringbad.cpp
#include <cstring> | |
#include "stringbad.h" | |
using std::cout;  | |
int StringBad::num_strings = 0;  | |
StringBad::StringBad() {  | |
len = 12;  | |
str = new char[12];  | |
strcpy_s(str, len,"c++");  | |
num_strings++;  | |
cout << num_strings << ": \"" << str << "\" object create\n";  | |
} | |
StringBad::StringBad(const char* s) {  | |
len = strlen(s);  | |
str = new char[len + 1];  | |
strcpy_s(str, len+1,s);  | |
num_strings++;  | |
cout << num_strings << ": \"" << str << "\" object create\n";  | |
} | |
StringBad::~StringBad() {  | |
cout << "\"" << str << "\" object delete, ";  | |
num_strings--;  | |
cout <<"left:" << num_strings << " \n";  | |
delete[] str;  | |
} | |
std::ostream& operator<<(std::ostream& os, const StringBad& st) {  | |
os << st.str;  | |
return os;  | |
} | 
main.cpp
#include "stringbad.h" | |
#include <iostream> | |
using std::cout;  | |
using std::endl;  | |
int main()  | |
{ | |
StringBad sb{ "Hello World!" };  | |
} | 
一切看上去都很合理,程序也能够正常运行~:

# 按值传递问题:
如果把 StringBad 类作为参数按值传递,将会引发程序异常中断。
void call(StringBad bad){  | |
	// do something... | |
} | |
StringBad b{ "test" };  | |
call(b);  | 
按值传递会导致对象 bad 和 b 内私有成员 char* str 共享一块 new 分配的内存,当 call 执行完毕后需要调用 bad 的析构函数。此时 char* str 已经被释放。然而在退出 main 时,需要对 b 进行释放,此时 char* str 被释放了两次最终导致程序异常。
# 赋值语句问题:
另一个容易产生误会的问题 —— 赋值。把一个 StringBad 对象赋值给另一个 StringBad 对象,会导致程序异常中断。
StringBad sb{ "Hello World!" };  | |
StringBad b = sb; // same: StringBad b = StringBad(sb);  | 
当执行上述赋值操作时,编译器将自动生成一个构造函数 StringBad(const StringBad&); 。该构造函数称为「复制构造函数」。
最终导致 char* str 被释放了两次。此外这两种方式都导致了类变量 static int num_strings; 没有被正常的更新。
# 特殊成员函数
C++ 编译器会在没有定义如下函数的情况下,自动生成默认的函数定义。
- 默认构造函数
 - 默认析构函数
 - 复制构造函数
 - 赋值运算符
 - 地址运算符
 - 移动构造函数
 - 移动赋值运算符
 
# 默认构造函数
如果没有定义任何构造函数,那么 C++ 将创建默认构造函数:
StringBad::StringBad(){} // 不接受参数  | 
带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值:
StringBad::StringBad(int len=10){}  | 
但是默认构造函数只能存在一个。
# 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。复制构造函数的原型通常如下:
ClassName::ClassName(const ClassName &);  | 
# 何时被调用❓
StringBad old {"old string bad"};  | |
StringBad new_1(old); // #1 call StringBad::StringBad(const StringBad &);  | |
StringBad new_2 = old; // #2 call StringBad::StringBad(const StringBad &);  | |
StringBad new_3 = StringBad(old); // #3 call StringBad::StringBad(const StringBad &);  | |
String* p_new_4 = new StringBad(old); // #4 call StringBad::StringBad(const StringBad &);  | 
其中 #2 和 #3 可能在调用复制构造函数时生成一个临时对象,然后把临时对象赋值给新创建对象(取决于实现细节)。
C++ 会在任何生成临时变量的地方调用复制构造函数来创建临时变量:
- 临时存储的临时变量 
a = b + c其中b + c会生成临时变量。 - 参数是值传递。
 - 作为返回值返回。
 
# 有何作用❓
默认复制构造函数会逐个复制「非静态成员」,复制的是成员的值(浅复制)。
如果成员本身就是类对象,那么会用该类的「复制构造函数」复制成员对象。

# 如何解决刚才 StringBad 的问题
# 定义一个显式复制构造函数
通过深拷贝的方式,解决同一个 const char* str 被释放多次的问题。
StringBad::StringBad(const StringBad& st) {  | |
num_strings++;  | |
len = st.len;  | |
str = new char[len + 1];  | |
strcpy_s(str, len + 1, st.str);  | |
cout << num_strings << ": \"";  | |
cout << str;  | |
cout << "\" deepcopy object create\n";  | |
} | 

如果类中包含了一个
new创建的对象,那么应该为其自定义一个复制构造函数,用来复制new所指向的数据,而非地址。
# 定义一个赋值运算符
你可能就会问,既然已经有了复制构造函数,那么为什么还需要定义赋值运算符呢?
因为赋值构造函数会在对象初始化的时候调用,但如果对象已经初始化完成了,再通过 = 赋值时,就会调用赋值运算符函数。
StringBad sb{ "Hello World!" };  | |
StringBad s = sb; //is ok call 「复制构造函数」  | |
sb = s; //is fail call 「赋值运算符函数」  | 
「赋值运算符」的原理和「复制构造函数」类型,但是还有更多的细节需要注意:
- 由于对象已经被初始化,在赋值前,需要清理原有的数据。
 - 函数应该避免赋值给自身,否则在对象赋值前,将会被释放。
 - 函数需要返回一个对象的引用。
 
StringBad& StringBad::operator=(const StringBad& st) {  | |
if (this == &st)return *this;  | |
delete[] str;  | |
len = st.len;  | |
str = new char[len + 1];  | |
strcpy_s(str, len + 1, st.str);  | |
return *this;  | |
} | 
# 定义其他成员函数来扩展 StringBad 的功能
# 比较成员函数
public:  | |
friend bool operator<(const StringBad& st_1, const StringBad& st_2);  | |
friend bool operator==(const StringBad& st_1, const StringBad& st_2);  | |
friend bool operator>(const StringBad& st_1, const StringBad& st_2);  | |
bool operator<(const StringBad& st_1, const StringBad& st_2) {  | |
return bool(std::strcmp(st_1.str, st_2.str) < 0);  | |
} | |
bool operator==(const StringBad& st_1, const StringBad& st_2) {  | |
return bool(std::strcmp(st_1.str, st_2.str) == 0);  | |
} | |
bool operator>(const StringBad& st_1, const StringBad& st_2) {  | |
return st_2 < st_1;  | |
} | 
# 使用中括号访问字符
定义操作符函数 [] 。
public:  | |
char& operator[](int i);  | |
char& StringBad::operator[](int i) {  | |
return str[i];  | |
} | 
# 静态类成员函数
在类申明中定义静态类函数,该函数可以直接通过类名调用,不需要类对象。
public:  | |
static int HowMany() { return num_strings; }  | |
StringBad::HowMany();  | 
# 赋值运算符重载
假设需要通过 getline() 读取字符并且构建 StringBad 对象。常规做法如下:
StringBad sb; | |
char temp[40];  | |
cin.getline(temp, 40);  | |
name = temp;  | 
还可以更简化~,使用 >> 来做:
// define | |
public:  | |
friend std::istream& operator>>(std::istream& is, StringBad& st);  | |
// come true | |
std::istream& operator>>(std::istream& is, StringBad& st) {  | |
char temp[40];  | |
is.get(temp, 40);  | |
if (is)st = temp;  | |
while (is && is.get() != '\n') continue;  | |
return is;  | |
} | |
// use | |
StringBad b; | |
cin >> b;  | 
# C++ 空指针
在 C++98 中,0 既可以表示数值,也可以表示空指针。这往往很难区分,部分程序会用 NULL 来代表空指针(C 语言的宏), C++11 提供了更好的方法 —— nullptr 。 str = nullptr; // C++ 11 null pointer notation
# 在构造函数中使用 new 时应注意的事项
- 如果在构造函数中使用 
new来初始化成员,则应该析构函数中delete。 new和delete必须互相兼容。new对应于delete,new[]对应于delete[]。- 如果存在多个构造函数,必须用相同的方式使用 
new,以保证析构函数可以全部适配。 delete操作无论是否带[],都可以对nullptr或者0使用。所以构造函数可以用nullptr或者new或者0来初始化指针。- 如果构造函数中存在 
new成员,请定义「复制构造函数」,用来进行深拷贝。 - 如果构造函数中存在 
new成员,请定义「赋值运算符函数」,用于检查自我赋值,释放旧数据,完成新数据的深拷贝。 
# 有关返回对象的说明
当函数返回对象时,有以下几种返回方式:
- 返回 
const对象的地址。 - 返回对象的地址。
 - 返回对象的值。
 - 返回 
const对象。 
# 返回 const 对象的地址
使用 const& 作为返回值本质上就是希望能够提高效率,传递引用远比传递值来的高效。
// #1 | |
StringBad test_return_const_addr(const StringBad& s1, const StringBad& s2) {  | |
if (s1 > s2) return s1;  | |
return s2;  | |
} | |
// #2 | |
const StringBad& test_return_const_addr_1(const StringBad& s1, const StringBad& s2) {  | |
if (s1 > s2) return s1;  | |
return s2;  | |
} | 
- 返回对象 
#1会调用「复制构造函数」,但返回引用不会。 #2所做的工作更少,效率更高。- 引用指向的对象应该在函数执行过程中一直存在。
 s1和s2都是const类型,因此返回值也必须为const类型。
# 返回指向对象的地址
常见的使用场景是:
- 重载赋值运算符以实现多次赋值。 
s1 = s2 = s3;。 - 重载 
<<运算符配合cout一起食用😄。cout << s1 << s2 << s3 << endl;。 
# 返回对象
如果被返回的对象在函数接受后将会被销毁,那么返回其引用将没有任何意义。这种情况下通常返回对象。
StringBad s_1{"string1"};  | |
StringBad s_2{"string2"};  | |
StringBad s_3 = s_1 + s_2;  | |
StringBad StringBad::operator+(const StringBad& s){  | |
return StringBad(str + s.str);  | |
} | 
# 返回 const 对象
上述操作虽然能够让你使用 + 操作符很好的完成计算。但是也会有一些不好之处:
s_1 + s_2 = "hello world"; // 这条语法在 返回对象 时同样适用  | 
为此,返回 const 对象将可以避免上述情况的发生。
# 使用 new 初始化对象
通常情况下 ClassName *p = new ClassName(value); 将会调用如下构造函数 ClassName(ValueType) ,或者是 ClassName(const ValueType&); ,这取决于函数如何定义。另外,如果没有二义性问题的情况下: ClassName *p = new ClassName; 将会执行默认构造函数 ClassName(); 。
# 再谈 new 和 delete
- 如果对象是动态变量,当退出动态变量的作用域时,将自动调用该对象的析构函数。
 - 如果对象时静态变量,则将在程序结束时调用对象的析构函数。
 - 如果对象是用 new 创建的,则必须显示的调用 delete 对对象进行回收。
 

# 指针和对象小结
- 使用常规表示法来声明指向对象的指针: 
String *p;。 - 可以将指针初始化为指向已有的对象: 
String *p = &array[0];。 - 可以使用 new 来初始化指针: 
String *p = new String("hello world");。 - 对类使用 new 来进行初始化,将调用类的构造函数:
 
String *p = new String; // String()  | |
String *p = new String("hello world"); // String(const char*)  | |
String *p = new String(&array[1]); // String(const String&)  | 
可以使用
->运算符通过指针访问类方法。p->length();。可以通过
*获取指针所指向的对象。(*p).str;。


# 其他类的操作
# 嵌套结构和类
在类的声明中声明的结构、类或枚举被称为是被嵌套在类中。其作用域为整个类。这种声明本身不会创建实例,只是定义了一个类型。如果该声明被定义为 private 的,那么只能在类中使用。如果是 public 的,则可以通过类对象进行访问。
typedef int Item;  | |
class Queue {  | |
private:  | |
    // node scope in class | |
struct Node {  | |
		Item item; | |
struct Node* next;  | |
};  | |
enum{Q_SIZE = 10};  | |
	// member | |
Node* front;  | |
Node* rear;  | |
	// ... | |
};  | 
# 成员初始化列表的语法
在构造函数后面,可以接上需要初始化的成员变量。但有几点需要注意:
- 该格式只能用于构造函数。
 
class Cls {  | |
private:  | |
int m;  | |
int n;  | |
int a;  | |
int b;  | |
int c;  | |
public:  | |
Cls(int, int);  | |
};  | |
// init a = _n, b = 1, c = _n*_n | |
Cls::Cls(int _n, int _m) :a(_n), b(1), c(_n* _n) {  | |
m = _m;  | |
n = _n;  | |
} | 
- 非静态 
const数据成员必须用这种方式初始化。(C++11 之前是) 
class CCls {  | |
private:  | |
const int a;  | |
public:  | |
CCls();  | |
};  | |
CCls::CCls() : a(10) {  | |
} | 
- 引用数据成员必须用这种方式初始化。
 
class Clls {  | |
private:  | |
	Cls cls; | |
public:  | |
Clls();  | |
};  | |
// 初始化引用数据 cls 必须采用该方法 | |
Clls::Clls():cls(1, 2) {  | |
} | 
# 初始化顺序
当初始化列表包含多个项目时,这些项目的被初始化顺序为它们被声明的顺序,而并非初始化列表中的顺序。
# C++11 的类内初始化
C++11 支持您以更直观的方式进行初始化。
class Cls {  | |
private:  | |
int m = 1;  | |
int n = 1;  | |
	//... | |
} | |
Cls::Cls():m(1), n(1) {...} // 两者等效  |