c++基础篇

今年的要学习一门语言,加上目前从事的工作需要c++,恰好有实践的机会,才开始了c++之旅

函数匹配

先来看一段代码。

1
2
3
4
5
6
7
8
9
template<class T>
T add(T a, T b){
return a+b;
}

double add(double a, double b){

return a+b;
}

由于函数重载,及函数模版的存在,c++需要一个良好的策略来决定调用哪一个函数,这个过程被称为重载解析</code,具体过程如下:

  1. 创建候选函数列表,其中包含与被调用函数的名称相同的函数和模版函数。
  2. 使用候选函数列表创建可行函数列表,其中参数数目都是正确的,这其中会存在一个隐士的类型转换。
  3. 确定最佳可执行函数。

判断最佳可执行函数的优先级如下:

  1. 完全匹配,但常规函数优先于模板函数
  2. 提升转换:charshort转换为intfloat转换为double
  3. 标准转换:long转换为double
  4. 用户定义的转换,如类中定义的转换。

左右值,与引用。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class TestA {
public:
int age;
TestA() {
std::cout << "ctor" << std::endl;
};
TestA(int a) {
std::cout << "TestA ctor" << std::endl;
};
//复制构造函数
TestA(const TestA &) {
std::cout << "copy ctor" << std::endl;
}
//移动构造函数
TestA(TestA &&t) {
this->age = t.age;
std::cout << "move ctor" << std::endl;
}
//复制赋值
TestA &operator=(const TestA & t) {
std::cout << "copy operator=" << std::endl;
return *this;
}
//移动赋值
TestA &operator=(TestA && t) {
std::cout << "move operator=" << std::endl;
this->age = t.age;
t.age = -1;
return *this;
}
~TestA() {
std::cout << "dtor" << std::endl;
}

virtual void test(){
std::cout<<"TestA test"<< std::endl;
}
};

class TestB : public TestA {
public:

virtual void test(){
std::cout<<"TestB test"<< std::endl;
}
TestB() {
std::cout << "TestB ctor" << std::endl;
};

virtual ~TestB() {
std::cout << "TestB dtor" << std::endl;
}
};

TestA &test(void) {
TestA *hello = new TestA;
return *hello;
}

TestA &&test2(void) {
TestA hello;
return std::move(hello);
}

TestA &&test3(void) {
TestA *hello = new TestA;
return std::move(*hello);
}

TestA test4(void) {
TestA hello;
return hello;
}

TestA &test5(void) {
TestA hello;
return hello;
}

TestA test6(TestA &&t) {
TestA hello;
hello.age = t.age;
return hello;
}

int main(int argc, const char * argv[]) {
// insert code here...
using namespace std;
std::cout << "Hello, World!\n";

TestA t1 = TestA();
TestA t2;
// t1.age = 10;
/* 构造函数 */
/* 如果“=”左边是引用类型,则不会调用额外的构造函数 */
/* 如果“=”右边是值类型,则要看返回值是否和“=”左边匹配,如不匹配,需要调用对应的构造函数 */
//返回&
TestA t4 = test();//复制构造函数,返回值为左值引用:&
TestA t6 = test5();//复制构造函数,返回值为左值引用:&
TestA &_t61 = test5();//定义引用类型,不需要构造

//返回&&
TestA t7 = test2();//移动构造函数,返回值为右值引用:&&
TestA t8 = test3();//移动构造函数,返回值为右值引用:&&
TestA t5 = test4();//=左右两边类型一样,不需要重复构造

TestA rd2 = std::move(t1);//移动构造
TestA &&rd3 = std::move(t1);//定义引用类型,不需要构造
TestA &&rd5 = std::move(t1);//定义引用类型,不需要构造
TestA &rd4 = rd3;//定义引用类型,不需要构造

t1.age = 200;

/* 赋值函数 */

TestA m2 = t1;//复制构造函数:t1是左值
rd2 = rd3;//复制赋值
rd2 = std::move(t1);//移动赋值
rd2 = test3();//移动赋值

m2 = t2;//复制赋值
rd3 = std::move(t2);//移动赋值
rd3 = rd5;//移动赋值

return 0;
}

c++的构造函数除默认的构造函数之外,还有复制构造函数移动构造函数

而赋值行为同样也有两类:复制赋值函数移动复制函数

而这些函数都是从默认的构造函数重载得来,也就是说,这些函数的调用是遵从重载解析的匹配规则的,需要主要的一点就是声明的右值引用类型也是左值,因此下面代码是调用复制赋值函数

1
2
TestA &&rd3 = std::move(t1);//定义引用类型,不需要构造
rd2 = rd3;//复制赋值

rd3虽然是右值引用变量,但也是左值。

虚函数表

c++中的函数地址绑定分为两种方式,一种为静态绑定static binding:在编译器直接确定函数的实现地址。
另一种为动态绑定dynamic binding:运行时确定函数的实现地址。

其中动态绑定是由虚函数表来实现。使用关键字virtual 声明为虚函数,主要来实现多态。

在不声明为test虚函数时,编译器将根据引用的类型指针的类型来确定应该执行的方法。

1
2
3
4
5
6
7
8
9
10
11
TestB b = TestB();
b.test(); //TestB test

TestA &a = b;
a.test(); //TestA test

TestA *c = &b
c->test(); //TestA test

TestA d = TestB()
d.test(); //???

如果test声明为虚函数,函数的执行将根据指针或引用的对象的类型来决定。

1
2
3
4
5
6
7
8
9
10
11
TestB b = TestB();
b.test(); //TestB test

TestA &a = b;
a.test(); //TestB test

TestA *c = &b
c->test(); //TestB test

TestA d = TestB()
d.test(); //???

这里需要可以思考下这两个例子中,最后一段代码的调用结果,结果上面说的到构造函数讲解。

1
2
TestA d = TestB()
d.test(); //???

下面来分解下:

  1. TestB() 将调用 TestB的构造函数,进而调用父类TestA的构造函数。并返回TestB的实例。
  2. 此时 “=”左边的类型为 TestA,右边为TestB的实例,因此将发生一次“移动构造函数”。TestB() 为右值。
  3. 移动构造函数结束,dTestA()的实例,因此将调用 TestA()test()的方法。

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。

智能指针

unique_ptr

遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有。同时不提供复制语义(复制赋值和复制构造都不可以),只支持移动语义。

1
2
3
4
5
6
7
8
9
void exampleMethod3(){
std::unique_ptr<TestA> uniqueA(new TestA());
{
std::unique_ptr<TestA> uniqueB = uniqueA;//复制构造 compile error :Call to implicitly-deleted copy constructor of 'std::unique_ptr<TestA>'
std::unique_ptr<TestA> uniqueC = std::move(uniqueA);//移动构造
}
//出了{} TestA 被释放
std::cout << "exampleMethod3 end!\n";
}

上面的实例首先定义了一个 unique_ptr uniqueA,后通过移动语言将控制权转交给unique_ptr uniqueC,由于uniqueC的作用域为里面的“{}”因此会在出了作用域时释放。

shared_ptr

shared_ptr有一个叫做共享所有权(sharedownership)的概念。其目标非常简单:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。也就是引用计数

1
2
3
4
5
6
7
8
9
10
11
//std::shared_ptr<TestA> shareB;
void exampleMethod2(){
std::shared_ptr<TestA> shareB;
{
TestA *aptr = new TestA();
std::shared_ptr<TestA> shareA(aptr);
shareB = shareA;
// refCount - 1
}
std::cout << "exampleMethod2 end!\n";
}

上面的实例同时有两个对TestA的引用,当“{}”的作用域结束,会进行引用计数-1,等待shareB的作用域结束进行释放。
如果把shareB的作用域提高到全局,则TestA的内存则会跟随程序的生命周期。