以下为个人学习笔记整理。参考书籍《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) {...} // 两者等效 |