Operators

在C++中可以重载运算符,实现自定义的运算。当原有运算符被重载后,只有当输入参数满足条件才会执行。

全局模板函数中重载运算符

1
2
3
4
5
template<typename T>
T operator+(const T& lhs,const T& rhs)
{
return a+b;
}

类模板中重载运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<typename T>
class MyClass
{
public:
MyClass(T v,T u):x(v),y(u){}

MyClass operator+(const MyClass& other) const
{
return MyClass(x+other.x,y+other.y);
}

//重载输出
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj)
{
os<<obj.x<<" "<<obj.y;
return os;
}

//重载赋值
MyClass& operator=(const MyClass& other)
{
if(this==&other)return *this;
x=other.x;
y=other.y;
return *this;
}


private:
T x,y;
}

注意在模板类中重载时,[],(),->,= 必须重载为成员函数,<< 必须重载为非成员函数,即友元函数。

对于操作符左右两边地位相等的操作,最好重载为友元函数,如+,< ,对于地位不等的,如 += ,重载为成员函数。

对于要改变左边的值的操作,如 +=,= ,在重载时需使用如下规定:

1
2
3
4
T& operator +=(const T& a)
{
...
}

即需要返回引用,这样支持链式操作。

重载STL相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
public:
Person(string name, int age) : name(name), age(age) {}

// 重载<
bool operator<(const Person& other) const {
return age < other.age; // 按年龄升序排序
}

private:
std::string name;
int age;
};

int main() {

// 使用 set,自动按 < 运算符排序
set<Person> peopleSet = {...};

// 使用 map,键自动按 < 运算符排序
map<Person, std::string> peopleMap = {...};

return 0;
}

POLA原则

即 $Principle\ of\ Least\ Astonishment (POLA)$ ,设计要符合常识。


Something about CLASS

Special member functions

1
2
3
4
5
6
7
8
9
10
class MyClass
{
public:
MyClass() = default;//默认构造函数
~MyClass() = default;//析构函数
MyClass(const MyClass& other) = default;//拷贝构造函数
MyClass& operator=(const MyClass& other) = default;//拷贝赋值运算符
MyClass(MyClass&& other) = default;//移动构造函数
MyClass& operator=(MyClass&& other) = default;//移动赋值运算符
};

注意默认拷贝构造函数、运算符一般是浅拷贝(STL某些事深拷贝)。实现自己的功能,可以自定义特殊函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class MyClass {
public:
// 1. 默认构造函数
MyClass() : number(0), ptr(new int(0)), text(new char[1])
{
text[0] = '\0';
std::cout << "Default constructor called\n";
}

// 2. 析构函数
~MyClass()
{
delete ptr;
delete[] text;
std::cout << "Destructor called\n";
}

// 3. 拷贝构造函数
MyClass(const MyClass& other) : number(other.number), ptr(new int(*other.ptr)), text(new char[std::strlen(other.text) + 1])
{
std::strcpy(text, other.text);
std::cout << "Copy constructor called\n";
}

// 4. 拷贝赋值运算符
MyClass& operator=(const MyClass& other)
{
std::cout << "Copy assignment operator called\n";

if (this == &other)return *this;

number = other.number;
delete ptr;
delete[] text;

ptr = new int(*other.ptr);
text = new char[std::strlen(other.text) + 1];
std::strcpy(text, other.text);

return *this;
}

// 5. 移动构造函数
MyClass(MyClass&& other) noexcept:
number(std::move(other.number)),
ptr(std::move(other.ptr)),
text(std::move(other.text))
{
std::cout << "Move constructor called\n";
other.ptr = nullptr;
other.text = nullptr;
}

// 6. 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept
{
std::cout << "Move assignment operator called\n";

if (this == &other) return *this;

delete ptr;
delete[] text;

ptr = std::move(other.ptr);
text = std::move(other.text);
number = std::move(other.number);

other.ptr = nullptr;
other.text = nullptr;
other.number = 0;

return *this;
}

private:
int number;
int* ptr;
char* text;
};

int main() {
MyClass obj1; // 调用默认构造函数

MyClass obj2(obj1); // 调用拷贝构造函数

MyClass obj3;
obj3 = obj1; // 调用拷贝赋值运算符

MyClass obj4(std::move(obj1)); // 调用移动构造函数

MyClass obj5;
obj5 = std::move(obj2); // 调用移动赋值运算符
return 0;
}

3 principles

$Rule\ of\ 0$:在设计类时尽量避免自定义任何特殊成员函数(不需要管理任何资源)。

$Rule\ of\ 3$:需要同时定义析构函数、拷贝构造函数、拷贝赋值运算符。

$Rule\ of\ 5$:需要同时定义析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。

Inheritance

基础用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public:
int public_value;
protected://子类可以访问,外部不能访问
int protected_value;
private://子类和外部都不能访问
int private_value;
};

class DerivedPublic : public Base {
// public_value 是 public 的
// protected_value 是 protected 的
// private_value 不能被访问
};

class DerivedProtected : protected Base {
// public_value 是 protected 的
// protected_value 是 protected 的
// private_value 不能被访问
};

class DerivedPrivate : private Base {
// public_value 是 private 的
// protected_value 是 private 的
// private_value 不能被访问
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base {
public:
Base() {
std::cout << "Base constructor called" << std::endl;
}
~Base() {
std::cout << "Base destructor called" << std::endl;
}
};

class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor called" << std::endl;
}
~Derived() {
std::cout << "Derived destructor called" << std::endl;
}
};
/*
Base constructor called
Derived constructor called
Derived destructor called
Base destructor called
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
Base(int x) {
std::cout << "Base constructor called with " << x << std::endl;
}
};

class Derived : public Base {
public:
Derived(int x) : Base(x) { // 调用基类的构造函数
std::cout << "Derived constructor called" << std::endl;
}
};

另外,若一个类含有纯虚函数,那么这个类就叫做抽象类,自身不能实例化,它的子类创建时必须实现这个功能。同时也有非纯虚函数。

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
virtual void show() = 0; // 纯虚函数
};

class Derived : public Base {
public:
void show() override {
std::cout << "Derived show()" << std::endl;
}
};

L-value and R-value

Basic

值分为左值与右值,同时还存在一类值叫将亡值($xvalue$),同属于两类值。

左值为可以取地址的值,如:

1
2
3
4
int a = 3;
int* p = &a;
*p;
const int b = 2;

右值为不可取地址的值,如字面常量,表达式返回值等:

1
2
3
10;              
x + y;
fmin(x, y);

注意的是,++a 为左值,a++ 为右值。

对于引用语法,无论左值引用还是右值引用,都是给对象取别名。

1
2
3
4
5
6
7
8
9
int val = 2;
int* ptr = 0x02248837;
vector<int> v1{1, 2, 3};

auto& ptr2 = ptr; // ptr2 is an l-value reference
auto&& v4 = v1 + v2; // v4 is an r-value reference
auto& ptr3 = &val; // ERROR: can't bind l-val ref to r-value
auto&& val2 = val; // ERROR: can't bind r-val ref to l-value
const auto& ptr3 = ptr + 5; // OKAY: CAN bind const l-val ref to r-value

特别的,对于 const 左值引用 可以引用右值。右值被引用后,如 v4 ,虽然它是右值引用变量,但是它也是左值,可以对 v4 取地址与赋值操作。

std::move

std::move(x) 强制把 x 变为右值引用,但是本身并不移动任何东西,这个操作只是为了调用对象的移动构造函数或者移动赋值运算符,具体清理被调用的对象过程在移动构造函数或移动赋值运算符中实现。

右值引用可以接受左值的 std::move(x)

应用:比如需要写一个泛型的交换函数:

1
2
3
4
5
6
7
template<typename T>
void my_swap(T& a,T& b)
{
T temp=std::move(a);
a=std::move(b);
b=std::move(temp);
}

Application

左值在应用中有着实际意义。传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func1(string s)
{...}

void func2(const string& s)
{...}


int main()
{
string s1("Hello World!");
func1(s1); // 由于是传值传参且做的是深拷贝,代价较大
func2(s1); // 左值引用做参数减少了拷贝,提高了效率

return 0;
}

但是缺陷在于对于在函数体执行结束后就会被析构的对象,就无法返回引用。而右值引用的出现弥补了这一短板,编译器在编译时会自动将返回值优化为移动构造。移动赋值。

另外对于大型对象,如果存在移动处理,那么使用 std::move() ,就会很有用,将拷贝变成了移动操作,减少了空间消耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyClass {
public:
//...
// 移动构造函数
MyClass(MyClass&& other) noexcept:
number(std::move(other.number)),
ptr(std::move(other.ptr)),
text(std::move(other.text))
{
std::cout << "Move constructor called\n";
other.ptr = nullptr;
other.text = nullptr;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept
{
std::cout << "Move assignment operator called\n";

if (this == &other) return *this;

delete ptr;
delete[] text;

ptr = std::move(other.ptr);
text = std::move(other.text);
number = std::move(other.number);

other.ptr = nullptr;
other.text = nullptr;
other.number = 0;

return *this;
}
//...
};

int main() {
MyClass obj1,obj2;
MyClass obj3(std::move(obj1)); // 调用移动构造函数

MyClass obj4 = std::move(obj2); // 调用移动赋值运算符
return 0;
}

移动构造函数、移动赋值运算符与拷贝构造函数、拷贝赋值运算符最大的区别就是,前者不用拷贝,直接移动,对于大型对象的处理很有用。但是在使用移动构造时要保证之后不再使用被移动的对象。

Perfect forwarding

对于确定类型的 && 表示右值引用,但是对于不确定类型T&& 就表示 万能引用 ,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。

完美转发 是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。然而根据以上知识,右值引用仍是左值,所以无法完美转发。为了实现这个功能,要用到 std::forward 来传递参数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void Func(int& x) { cout << "左值引用" << endl; }

void Func(const int& x) { cout << "const左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }

void Func(const int&& x) { cout << "const右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t) // 万能引用
{
Func(std::forward<T>(t)); // 根据参数t的类型去匹配合适的重载函数
}

int main()
{
int a = 4; // 左值
PerfectForward(a);

const int b = 8; // const左值
PerfectForward(b);

PerfectForward(10); // 10是右值

const int c = 13;
PerfectForward(std::move(c)); // const左值被move后变成const右值

return 0;
}

完美转发主要应用于提升模板函数的泛化能力,创建通用工厂函数,提高代码的灵活性和可维护性,减少拷贝和重复构造等,在库函数和泛型编程中应用很多。


RAII and Smarter Pointers

RAII

RAII是一种思想,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

Smarter Pointers

所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。C++11的智能指针包括三类:unique_ptr,shared_ptr,weak_ptr

在于 new 了一个对象后,需要自己 delete 才能释放资源,若不 delete 是不会自动调用析构函数的。而如果忘记了 delete ,就有可能导致内存泄漏。而智能指针则能自动释放。

unique_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//构造
unique_ptr<Test> t1;

unique_ptr<Test> t2(new Test);

unique_ptr<int[]> t3;

unique_ptr<int[]> t4(new int[5]);

//赋值
unique_ptr<Test> t7(new Test);
unique_ptr<Test> t8(new Test);
t7 = std::move(t8); // 必须使用移动语义

vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));

vec.push_back(std::move(p3));
vec.push_back(std::move(p4));
vec[0] = std::move(vec[1]); // 需要使用move修饰

//其他操作
unique_ptr<Test>t;
Test new_t=t.release();//放弃t的控制权
t.reset(new Test);//重置为一普通指针并智能托管

缺点:不能多个智能指针托管同一个指针,由于排他性会产生问题。

shared_ptr

原理是可以记录特定内存对象的智能指针数量,复制或拷贝时引用次数+1,析构是引用次数-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
shared_ptr<Person> sp1;
shared_ptr<Person> sp2(new Person(2));

// 获取智能指针管控的共享指针的数量 use_count():引用计数
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;

//构造
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2

shared_ptr<int> up3 = make_shared<int>(2);
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);//更推荐用make_shared

//赋值
shared_ptrr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(new int(11)); // int(11) 的引用计数为1
up1 = up2;//int(10) 的引用计数为0,int(11) 的引用计数为2

//释放
//重置
//交换
std::swap(p1,p2);
p1.swap(p2);

缺点:两个类各持有对方的管控成员,会造成 count 不为零0,从而不能调用析构函数。

weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载 *-> 但可以使用 lock 获得一个可用的 shared_ptr 对象,用完后记得置 null