现代C++学习笔记

本文最后更新于:6 个月前

C++的基本特性

程序的执行过程

程序被执行后就被称为一个进程,一个进程可以被划分为很多区域。

其中比较重要的是以下四个区域。

1代码区与常量区:进程按照代码区的代码执行,真正的常量也存储在这里,比如“abc”字符串,“1”,“88”等数字。这些是真正的常量。再看一下const关键字。const只不过是让编译器将变量视为常量罢了,和真正的常量有本质上的区别

2栈区:函数的执行所需的空间,注意,当函数执行完毕,函数对应的栈内存全部销毁。

3堆区:进程用来分配内存的地方,只有手动释放才能销毁内存。

4静态变量区:

(1)静态变量:常常遇到的一些局部作用范围,生命周期却很长的变量。

(2)全局变量:在c++中不建议使用,会破坏封装性。


堆和栈的关系

堆区有灵活的生命周期。如果需要创建的对象有几十M,每次调用函数都需要创建一个这么大的对象,再复制到对应的容器中,那就太过耗费内存了。而且栈内存非常的小,通常不超过8M。而使用堆内存,每调用一次函数就可以在堆内存中创建一个对象,容器中只要存储指针就可以了,极大的提高了程序效率。栈区是函数执行的区域,堆区是函数内灵活分配内存的地方,二者缺一不可。堆的唯一寻址方式就是指针,如果没有栈,根本无法使用堆。

(*) new 关键字及内存泄漏

1.new关键字是c++用来动态分配内存的主要方式.

1
2
3
4
5
6
7
8
9
#include<isotream>

int main()
{
int* pi = new int(100);
std::cout << *pi << std::endl;
delete pi;
return 0;
}

new可以直接分配单个变量的内存,也可以分配数组。

1
2
3
4
5
6
7
8
9
#include<iostream>

int main()
{
int* pi = new int[100]();//小括号初始化为零,没有小括号分配未定义的内存,而且不可以赋初值
std::cout << pi[20] << std::endl;
delete[] pi;//不加中括号会导致动态内存泄露
return 0;
}

在分配单个对象的内存时,

当对象是普通变量时,可以分配对应的内存

当对象是类对象时,会调用构造函数,如果没有对应的构造函数,就会报错。

1
2
3
4
5
6
7
8
9
#include<iostream>

int main()
{
std::string* pString = new std::string("hello world");//如果是字符串数组的话不能赋初值
std::cout << *pString << std::endl;
delete pString;
return 0;
}

在分配数组对象内存时:

对于普通变量:可以使用“()”将所有对象全部初始化为0。

对于类对象,有没有“()”都一样,均使用默认构造函数,如果没有默认构造函数就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>

class Test
{
public:
Test(int i_) :i(i_){}
private:
int i;

};
int main()
{
Test* pTest = new Test[100];//这是错误的,类Test不存在默认构造函数
delete[] pTest;
return 0;
}

2内存泄漏

​ 内存泄露会导致堆内存的逐渐被占用,最终内存用完程序崩溃。常见的情况就是项目测试没问题,上线几天就炸了。然后就会非常麻烦,排查困难,损失很大。

内存泄露是最严重的错误之一,程序不怕报错,就怕一开始运行的好好的,突然就出现了莫名其妙的错误。

这句话也引出了后面的两个部分。(期待学习hhh)

Part4的智能指针可以非常好的避免内存泄露的问题。

Part9的异常处理部分可以恰当的处理程序出现的异常,让程序有错误就立马处理,或直接终止进程,或忽略,不要让异常莫名其妙。这是程序设计的重要理念。

命名空间

C++经常需要多个团队合作来完成大型项目。多个团队就常常出现起名重复的问题,C++就提供了命名空间来解决这个问题。

例子

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
ATest.h
#pragma once
void test();
BTest.h
#pragma once
void test();
BTest.cpp
#include "BTest.h"
#include<iostream>
void test()
{
std::cout << "B::()" << std::endl;
}
ATest.cpp
#include "ATest.h"
#include<iostream>
void test()
{
std::cout << "A::()" << std::endl;
}
main.cpp
#include<iostream>
#include"ATest.h"
#include"BTest.h"
int main()
{
test();//报错,不知道调用哪个test函数
return 0;
}

解决(使用命名空间)

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
ATest.h
#pragma once
namespace A
{
void test();
}


BTest.h
#pragma once
namespace B
{
void test();
}


BTest.cpp
#include "BTest.h"
#include<iostream>
namespace B
{
void test()
{
std::cout << "B::test()" << std::endl;
}
}


ATest.cpp
#include "ATest.h"
#include<iostream>
namespace A
{
void test()
{
std::cout << "A::test()" << std::endl;
}
}


main.cpp
#include<iostream>
#include"ATest.h"
#include"BTest.h"
int main()
{
B::test();
A::test();
return 0;
}

顺便提两点

命名空间的实现原理,C++最后都要转化为C来执行程序。在namespace A中定义的Test类,其实全名是A::Test。C++所有特有的库(指c没有的库),都使用了std的命名空间。比如最常用的iostream。

using关键字设计的目的之一就是为了简化命名空间的。using关键字在命名空间方面主要有两种用法。

  1. using 命名空间::变量名。这样以后使用此变量时只要使用变量名就可以了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    main.cpp
    #include<iostream>
    #include"ATest.h"
    #include"BTest.h"
    using A::test;
    using B::test;//同时使用会报错
    int main()
    {
    test();
    return 0;
    }
  2. using namspce 命名空间。这样,每一个变量都会在该命名空间中寻找。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    main.cpp
    #include<iostream>
    #include"ATest.h"
    #include"BTest.h"
    using namespace A;
    using namespace B;//同时使用会报错
    int main()
    {
    test();
    return 0;
    }

    所以,头文件中一定不能使用using关键字。会导致命名空间的污染

    1
    2
    3
    4
    5
    6
    7
    8
    错误代码
    ATest.h
    #pragma once
    namespace A
    {
    void test();
    }
    using namespace A;

(*)C++的标准输入输出简介

输入输出简单来说就是数据在输入设备,内存,硬盘,输出设备之间移动的过程。

c语言设定了很多不相关的函数还实现这些过程。

比如printf就是让数据从内存到显示屏(显示屏就是输出设备)。scanf就是让数据从键盘(键盘是输入设备)到内存。此外还有从内存到磁盘的文件操作函数。

c语言的函数虽然简单方便,但彼此之间没有关联。C++有了继承功能,可以让子类与父类之间有关联性,极大的提高各种输入输出功能之间的耦合性。

于是C++用继承功能重写了输入输出功能,这就是io库,io库引入了“流”的概念,数据从一个地方到另一个地方,原本地方的数据就没了,叫做流很贴切。

io库是一个很大的部分,但现阶段我们只要会使用输入输出流,cout和cin就可以了。

cout可以让数据从内存流到输出设备,cin可以让数据从输入设备流到内存。

const关键字的介绍

const是让编译器将变量视为常量,用const修饰的变量和真正的常量有本质的区别。

  1. 真正的常量存储在常量区代码区,比如“abcdefg”这个字符串就存储在常量区,而“3”,“100”这些数字就存储在代码区中,这些都是真正的常量,无法用任何方式修改。

  2. const修饰的变量仍然存储在堆区栈区中,从内存分布的角度讲,和普通变量没有区别。const修饰的变量并非不可更改的,C++本身就提供了mutable关键字(这个关键字在Part3就会讲的)用来修改const修饰的变量,从汇编的角度讲,const修饰的变量也是可以修改的。

(**)auto关键词的使用

auto是C++11新加入的关键字,就是为了简化一些写法。

为了学习auto的类型推断,我使用一个boost库来确定变量的具体类型。boost库很大,可以选择编译自己想要的模块,我就直接全部编译了。boost是很复杂的,不是几句话能说清楚,要深入理解可以去官网学习。

演示

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
auto i = 100;
std:: cout << type_id_with_cvr<decltype(i)>().pretty_name() << std::endl;
return 0;
}

来说一下auto,有好几个点需要注意:

**1.**auto只能推断出类型,引用不是类型,所以auto无法推断出引用,要使用引用只能自己加引用符号。

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
int i = 100;
auto& i2 = i;
std:: cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;//输出类型int &
return 0;
}

**2.**auto关键字在推断引用的类型时:会直接将引用替换为引用指向的对象。其实引用一直是这样的,引用不是对象,任何使用引用的地方都可以直接替换成引用指向的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
int i = 100;
const int& refi = i;
auto& i2 = i;
std:: cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;//int &
return 0;
}

3.auto关键字在推断类型时,如果没有引用符号,会忽略值类型的const修饰,而保留修饰指向对象的const,典型的就是指针。可能有些不好理解,看看代码就好说了。3和4的主要作用对象就是指针.

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
int i = 100;
const int* const pi = &i;//前者const修饰的是指针pi修饰的值,后者const修饰的是pi,后者const会被忽略
auto pi2 = pi;
std:: cout << type_id_with_cvr<decltype(pi2)>().pretty_name() << std::endl;//int const *=const int *
return 0;
}

例子2

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
const int i = 100;
auto i2 = i;
std:: cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;//int
return 0;
}

**4.**auto关键字在推断类型时,如果有了引用符号,那么值类型的const和修饰指向对象的const都会保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
const int i = 100;
const int* const pi = &i;

auto& pi2 = pi;
std:: cout << type_id_with_cvr<decltype(pi2)>().pretty_name() << std::endl;//int const * const &
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
const int i = 100;
auto& i2 = i;
std:: cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;//int const &
return 0;
}

其实3,4为什么会出现这种情况,因为在传递值时,修改这个值并不会对原有的值造成影响。而传递引用时,修改这个值会直接对原有的值造成影响。

**5.**当然,我们可以在前面加上const,这样永远都有const的含义。

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
const int i = 100;
const auto i2 = i;
std:: cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;//int const
return 0;
}
  1. auto不会影响编译速度,甚至会加快编译速度。因为编译器在处理XX a = b时,当XX是传统类型时,编译期需要检查b的类型是否可以转化为XX。当XX为auto时,编译期可以按照b的类型直接给定变量a的类型,所以效率相差不大,甚至反而还有提升。
  2. (*)最重要的一点,就是auto不要滥用,对于一些自己不明确的地方不要乱用auto,否则很可能出现事与愿违的结果,使用类型应该安全为先。
  3. (*)auto主要用在与模板相关的代码中,一些简单的变量使用模板常常导致可读性下降,经验不足还会导致安全性问题。

(*)静态变量,指针和引用

变量的存储位置有三种,分别是静态变量区,栈区,堆区。

**1.**静态变量区在编译时就已经确定地址,存储全局变量与静态变量。

演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>

unsigned g_i = 0;//全局变量,在程序编译时已经初始化
unsigned test()
{
static unsigned callCount = 0;//这行代码在编译时已经初始化,直接执行下一行
return ++callCount;//统计函数调用次数
}
int main()
{
test();
test();
unsigned testFuncCallCount = test();
++g_i;//程序运行时执行
std::cout << testFuncCallCount << std::endl;
return 0;
}

**2.**指针都是存储在栈上和堆上,不管在栈上还是堆上,都一定有一个地址。

本质上说,指针和普通变量没有区别。

在32位系统中,int变量和指针都是32位。指针必须和“&”,“*”这两个符号一起使用才有意义。

&a代表的a这个变量的地址,a代表的a对应地址存储的值,*a代表对应地址存储的值作为地址对应的值。

所以指针才可以灵活的操作内存,但这也带来了严重的副作用,比如指针加加减减就可以操作内存,所以引用被发明了,引用就是作用阉割的指针(可以视为“类型*const”,所以引用必须上来就赋初值,不能设置为空),编译器不将其视作对象,操作引用相当于操作引用指向的对象。也就从根本是杜绝了引用篡改内存的能力。

1
2
int i=20;
int& refI=i;//类似于int* const pi=&i;

(**)左值,右值,左值引用,右值引用

1.左值和右值

C++任何一个对象要么是左值,要么是右值。比如int i = 10,i和10都是对象

左值:拥有地址属性的对象就叫左值,左值可以放在等号右边,也可以放在等号左边

右值:不是左值的对象就是右值。无法操作地址属性的对象就是右值。比如临时对象,就都是右值,临时对象的地址属性无法使用。注意:左值也可以放在“=”右面,但右值绝对不可以放在等号左面

演示

1
2
3
int i=10;
int i2=i+1;//i+1为临时对象,有地址但无法使用地址
++i;//左值 i++为右值

2.引用的分类

(1) 普通左值引用:就是一个对象的别名,只能绑定左值,无法绑定常量对象。

(2) const左值引用:可以对常量起别名,可以绑定左值和右值

(3) 只能绑定右值的引用。

(4) 万能引用

1
2
3
4
5
6
int i=100;
int& refi=i;//只能绑定左值
const int& i;//绑定左值
const int& (i+1);//绑定右值
int&& rrefi=(i+1);//右值引用
int&& rrefi=i++;//右值引用

(**)move函数,临时对象

1.move函数

(1) 右值看重对象的值而不考虑地址,move函数可以对一个左值使用,使操作系统不再在意其地址属性,将其完全视作一个右值。

(2) move函数让操作的对象失去了地址属性,所以我们有义务保证以后不再使用该变量的地址属性,简单来说就是不再使用该变量,因为左值对象的地址是其使用时无法绕过的属性。

1
2
int i=0;
int&& rrefi=std::move(i);//std::move(i)整体是个右值,i能继续赋值

2.临时对象

右值都是不体现地址的对象。那么,还有什么能比临时对象更加没有地址属性呢?右值引用主要负责处理的就是临时对象。

程序执行时生成的中间对象就是临时对象,注意,所有的临时对象都是右值对象,因为临时对象产生后很快就可能被销毁,使用的是它的值属性。

1
2
3
4
5
6
7
8
9
10
#include<iostream>
int getI()
{
return 10;//return 的是一个临时对象,所有的临时对象都是右值
}
int main()
{
int i=10;
int&& rrefi=getI();//接收不到就会销毁
}

(**)可调用对象

如果一个对象可以使用调用运算符“()”,()里面可以放参数,这个对象就是可调用对象。

1.函数:函数自然可以调用()运算符,是最典型的可调用对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
void test(int i)
{
std::cout << i << std::endl;
std::cout << "hello world" << std::endl;

}
using pf_type = void(*)(int);
void myFunc(pf_type pf, int i)
{
pf(i);
}
int main()
{
myFunc(test,200);
return 0;
}

2.仿函数:具有operator()函数的类对象,此时类对象可以当做函数使用,因此称为仿函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
class Test
{
public:
void operator()(int i)
{
std::cout << i << std::endl;
std::cout << "void operator()(int i)" << std::endl;
}
};

int main()
{
Test t;
t(20);
return 0;
}

3.lambda表达式

就是匿名函数,普通的函数在使用前需要找个地方将这个函数定义,于是C++提供了lambda表达式,需要函数时直接在需要的地方写一个lambda表达式,省去了定义函数的过程,增加开发效率。

注意:lambda表达式很重要,现代C++程序中,lambda表达式是大量使用的

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
int main()
{
int i = 0;
[i] (int elem) {
std::cout << i << std::endl;
std::cout << elem << std::endl;
std::cout << "hello world" << std::endl;
}(200);
return 0;
}

lambda表达式的格式最少是“[] {}”,完整的格式为“[] () ->ret {}”。

lambda各组件介绍

lambda各个组件介绍

1.[]代表捕获列表:表示lambda表达式可以访问前文的哪些变量。

(1) []表示不捕获任何变量。

(2) [=]:表示按值捕获所有变量。

(3) [&]:表示按照引用捕获所有变量。

=,&也可以混合使用,比如

(4) [=, &i]:表示变量i用引用传递,除i的所有变量用值传递。

(5) [&, i]:表示变量i用值传递,除i的所有变量用引用传递。

当然,也可以捕获单独的变量

(6) [i]:表示以值传递的形式捕获i

(7) [&i]:表示以引用传递的方式捕获i

啊,part1结束,下周一定减少划水时间,多听课

类的权限修饰:c++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。

在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

(*)类介绍,构造函数,析构函数

1.类介绍:

(1) 对面向对象和面向过程的理解

① 面向对象和面向过程是一个相对的概念。

② 面向过程是按照计算机的工作逻辑来编码的方式,最典型的面向过程的语言就是c语言了,c语言直接对应汇编,汇编又对应电路。

③ 面向对象则是按照人类的思维来编码的一种方式,C++就完全支持面向对象功能,可以按照人类的思维来处理问题。

④ 举个例子,要把大象装冰箱,按照人类的思路自然是分三步,打开冰箱,将大象装进去,关上冰箱。

要实现这三步,我们就要首先有人,冰箱这两个对象。人有给冰箱发指令的能 力,冰箱有能够接受指令并打开或关闭门的能力。

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
//c++
#inlcude<iostream>
class IceChest;
class Person
{
public:
void openIceChest(const IceChest& iceChest)
{

}
}
class IceChest:
{
public:
void openDoor()
{

}
void closeDoor()
{

}
};
//c语言
struct Person
{

};
struct IceChest
{

};
void personOpenIceChest(const Person& person,const IceChest& iceChest)
void personCloseIceChest(const Person& person,const IceChest& iceChest)

但是从计算机的角度讲,计算机只能定义一个叫做人和冰箱的结构体。人有手这个部位,冰箱有门这个部位。然后从天而降一个函数,是这个函数让手打开了冰箱,又是另一个函数让大象进去,再是另一个函数让冰箱门关上。

从开发者的角度讲,面向对象显然更利于程序设计。用面向过程的开发方式,程序一旦大了,各种从天而降的函数会非常繁琐,一些用纯c写的大型程序,实际上也是模拟了面向对象的方式。

那么,如何用面向过程的c语言模拟出面向对象的能力呢?类就诞生了,在类中可以定义专属于类的函数,让类有了自己的动作。回到那个例子,人的类有了让冰箱开门的能力,冰箱有了让人打开的能力,不再需要天降神秘力量了,hh

2.构造函数

类是通过面向过程的机器实现的,类相当于定义了一个新类型,该类型生成在堆或栈上的对象时内存排布和c语言相同。但是c++规定,C++有在类对象创建时就在对应内存将数据初始化的能力,这就是构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<isotream>
struct CPPtest
{
public:
CPPTest(int i_,int i2_) :i(i_),i2(i2_){}//c++可以在类中构造函数,将参数对应内存初始化;若没有,则自动生成,但是没什么用
int i;
int i2;
};
int main(){
CPPtest cppTest(1,2);//初始化i,i2
std::cout<<cppTest.i<<std::endl;
std::cout<<cppTest.i2<<std::endl;
return 0;
}

构造函数有以下类型。

  1. 普通构造函数

  2. 复制构造函数:用另一个对象来初始化对象对应的内存

    1
    CPPTest(const CPPTest& cppTest) :i(cppTest.i), i2(cppTest.i2){}
  3. 移动构造函数:也是用另一个对象来初始化对象,具体内容会在Part3第13节详细讲解。

  4. 默认构造函数:当类没有任何构造函数时,编译期会为该类生成一个默认的的构造函数,在最普通的类中,默认构造函数什么都没做,对象对应的内存没有被初始化。

1
CPPTest(){};//作用和构造函数一样

构造函数就是C++提供的必须有的在对象创建时初始化对象的方法,(默认的什么都不做也是一种初始化的方式)

3.析构函数

介绍:

类对象被销毁时,就会调用析构函数。栈上对象的销毁时机就是函数栈销毁时,堆上的对象销毁时机就是该堆内存被手动释放时,如果用new申请的这块堆内存,那调用delete销毁这块内存时就会调用析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<isotream>
struct CPPTest
{
public:
CPPTest(int i_,int i2_) :i(i_),i2(i2_){}
CPPTest(const CPPTest& cppTest) :i(cppTest.i), i2(cppTest.i2){}
int i;
int i2;
~CPPTest()//如没有会自动添加
{

}
};
int main(){
CPPTest cppTest(1,2);//初始化i,i2
std::cout<<cppTest.i<<std::endl;
std::cout<<cppTest.i2<<std::endl;
CPPTest* pCppTest=new CppTest(1.2);//调用在堆上,自动销毁
delete PCppTest;
return 0;
}

但是再某些情况下,析构函数必须要干活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<isotream>
struct CPPTest
{
public:
CPPTest(int i_,int i2_) :i(i_),i2(i2_),pi(new int(i3)){}
CPPTest(const CPPTest& cppTest) :i(cppTest.i), i2(cppTest.i2),pi(new int(*cppTest.pi)){}
int i;
int i2;
int *pi;
~CPPTest()
{
delete pi;
}//当栈上pi指向堆上的i3,如果析构函数中没有delete,那么只会把栈上的释放,堆上的数据没有释放,会造成内存泄漏
};
int main(){
}

总结,当类对象销毁时有一些我们必须手动操作的步骤时,析构函数就派上了用场。所以,几乎所有的类我们都要写构造函数,析构函数却未必需要

(*)this,常成员函数与常对象

1.this关键字:

(1) this是什么:

① 编译器将this解释为指向函数所作用的对象的指针,这句话新手有些不好理解,用代码演示一下就好说了。C++类的本质就是C语言的结构体外加几个类外的函数,C++最后都要转化为C语言来实现,类外的函数就是通过this来指向这个类的。

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
#include<iostream>
clacc Test
{
public:
Test(const std::string& name_, unsigned old_);
~Test();
void output()const
{
std::cout<<"name="<<this->name<<" old="<<this->old<<std::endl;
}
private:
std::string name;
unsigned old;
};
Test::Test(const std::string& name_,unsigned old_):name(name_),old(old_)
{
}
Test::~Test()
{
}

int main()
{
Test test("fanfan",20);
test.outPut();
return 0;
}

② 当然,这么说并非完全准确,this是一个关键字,只是我们将它当做指针理解罢了。

this有很多功能是单纯的指针无法满足的。比如每个类函数的参数根本没有名叫this的指针。这不过是编译器赋予的功能罢了。

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
#include<iostream>
clacc Test
{
public:
Test(const std::string& name_, unsigned old_);
~Test();
void output()const
{
std::cout<<"name="<<this.name<<" old="<<this.old<<std::endl;
}
private:
std::string name;
unsigned old;
};
Test::Test(const std::string& name_,unsigned old_):name(name_),old(old_)
{
}
Test::~Test()
{
}
void (Test::* pf)()=&Test::output;//空参数的指针能指向类的成员函数,说明类中没有this这个形参变量
int main()
{
Test test("fanfan",20);
(test.*pf)();//结果一样
return 0;
}

2.常成员函数和常对象

某位大佬说:“常成员函数和常对象很多人并不在意,确实,都写普通变量也可以。但是,我还是要提一点,在大型程序中,尽量加上const关键字可以减少很多不必要的错误。这一点,开发过大型程序的人应该深有体会,没开发过大型程序的人也不必在意,记住多用const,这是一个很好的习惯。“

(1) 常成员函数就是无法修改成员变量的函数。可以理解为将this指针指向对象用const修饰的函数。(例子在本节的第一个代码演示)

常对象就是用const修饰的对象,定义好之后就再也不需要更改成员变量的值了。

(2) 常成员函数注意事项:

因为类的成员函数已经将this指针省略了,只能在函数后面加const关键字来实现无法修改类成员变量的功能了

① 注意:常函数无法调用了普通函数,无意义。

成员函数能写作常成员函数就尽量写作常成员函数,可以减少出错几率。

③ 同名的常成员函数和普通成员函数是可以重载的,常量对象会优先调用常成员函数,普通对象会优先调用普通成员函数。

(3) 常对象注意事项:

① 常对象不能调用普通函数

② 常函数在大型程序中真的很重要,很多时候我们都需要创建好就不再改变的对象

大佬强调:常对象和常函数要多用

inline,mutable,default,delete

inline,mutable知道就行,default和delete需要掌握

1.inline关键字

(1) inline关键字的有什么作用:

① 在函数声明或定义中函数返回类型前加上关键字inline就可以把函数指定为内联函数。关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前不起任何作用。

② 内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
class Test
{
public:
inline void outPut();//默认有inline,后边的不加,这里加了也是无效的
};
inline void Test::outPut()
{
std::cout<<"hello world"<<std::endl;
}
int main()
{
Test test;
test.outPut();
return 0;
}

(2) inline关键字的注意事项:inline关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联的情况编译器都会采纳建议。(牺牲空间换取时间)

(3) **总结:**使用inline关键字就是一种提高效率,但加大编译后文件大小的方式,现在随着硬件性能的提高,inline关键字用的越来越少了。

2.mutable关键字

(1) mutable关键字的作用:

① Mutable意为可变的,与const相对,被mutable修饰的成员变量,永远处于可变的状态,即便处于一个常函数中,该变量也可以被更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
class Test
{
public:
void outPut() const;
mutable unsigned outPutCallCount=0;
};
void Test::outPut()const
{
++outPutCallCount;
std::cout<<"hello world"<<std::endl;
}
int main()
{
Test test;
test.outPut();
std::cout<<test.outPutCallCount<<std::endl;
return 0;
}

这个关键字在现代C++中使用情况并不多,一般来说只有在统计函数调用次数时才会用到。

(2) mutable关键字的注意事项

① mutable是一种万不得已的写法,一个程序不得不使用mutable关键字时,可以认为这部分程序是一个糟糕的设计。

② mutable不能修饰静态成员变量和常成员变量

(3) 总结:mutable关键字是一种没有办法的办法,设计时应该尽量避免,只有在统计函数调用次数这类情况下才推荐使用。

3.default关键字

(1) default关键字的作用:

① 在编译时不会生成默认构造函数时便于书写。

② 也可以对默认复制构造函数,默认的赋值运算符和默认的析构函数使用,表示使用的是系统默认提供的函数,这样可以使代码更加明显。

1
2
3
4
5
6
7
8
9
10
#include<iostream>
class Test
{
Test(unsigned old_):old(old_){}
Test() = default;//以前Test(){}
Test(const Test& test)=default;//使用系统默认
Test& operator=(const Test& test)=default;
~Test()=default;
unsigned old;
};

现代C++中,哪怕没有构造函数,也推荐将构造函数用default关键字标记,可以让代码看起来更加直观,方便。

总结:default关键字还是推荐使用的,在现代C++代码中,如果需要使用一些默认的函数,推荐用default标记出来。

4.delete关键字

(1) Delete关键字的作用:C++会为程序生成默认构造函数,默认复制构造函数,默认重载赋值运算符。

1
2
3
4
5
6
7
8
9
#include<iostream>
class Test
{
//没有Test(unsigned old_):old(old_){},还不想默认生成
Test()=delete
//Test& operator=(const Test& test)=default;使用系统默认,不想使用就将default换成delete
~Test()=default;//一般不会delete
unsigned old;
};

在很多情况下,我们并不希望这些默认的函数被生成,在C++11以前,只能有将此函数声明为私有函数或是将函数只声明不定义两种方式。

C++11于是提供了delete关键字,只要在函数最后加上“=delete”就可以明确告诉编译期不要默认生成该函数。

总结:delete关键字还是推荐使用的,在现代C++代码中,如果不希望一些函数默认生成,就用delete表示,这个功能还是很有用的,比如在单例模式中。

友元类和友元函数

**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
#include<iostream>
class Test
{
friend class Test2;//友元类
friend void outPut(const Test& test);//友元函数
private:
std::string name;
unsigned old;
}
class Test2
{
public:
void outPut(const Test& test)
{
std::cont<<test.name<<" "<<test.old<<std::endl;//没有friend,类不能访问Test中的变量
}
}
void outPut(const Test& test)//上接友元函数
{
std::cont<<test.name<<" "<<test.old<<std::endl;//没有friend,函数不能访问Test中的变量
}
int main()
{

}

**2.**注意:

(1) 友元会破坏封装性,一般不推荐使用,所带来的方便写几个接口函数就解决了。

1
2
3
4
5
6
7
8
class Test
{
std::string getName()const{return name;};//
unsigned getOld()const{return old;};//接口
private:
std::string name;
unsigned old;
}//同样效果

(2) 某些运算符的重载必须用到友元的功能,这才是友元的真正用途。

**3.**大佬说:友元平常并不推荐使用,新手不要再纠结友元的语法了,只要可以用友元写出必须用友元的重载运算符就可以了。

(**)重载运算符

重载运算符在整个C++中拥有非常重要的地位

1.重载运算符的作用:

(1) 很多时候我们想让类对象也能像基础类型的对象一样进行作基础操作,比如“+”,“-”,“*”,“\”,也可以使用某些运算符“=”,“()”,“[]”,“<<”,“>>”。但是一般的类即使编译器可以识别这些运算符,类对象也无法对这些运算符做出应对,我们必须对类对象定义处理这些运算符的方式。

(2) C++提供了定义这些行为的方式,就是operator 运算符来定义运算符的行为,operator是一个关键字,告诉编译器我要重载运算符了。

2.注意:

(1) 我们只能重载C++已有的运算符,所有无法将“”这个运算符定义为指数的形式,因为C++根本没有“”这个运算符。

(2) C++重载运算符不能改变运算符的元数,“元数”这个概念就是指一个运算符对应的对象数量,比如“+”必须为“a + b”,也就是说“+”必须有两个对象,那么“+”就是二元运算符。比如“++”运算符,必须写为“a++”,也就是一元运算符。

3.重载运算符举例

(1) 一元运算符重载

① “++”,“–”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iosteam>
class Test
{
public:
void opterator++()
{
++count;
}
void opterator--()
{
--count;
}
unsigned count = 0;
}
int main()
{
Test test;
++test;
std::cout<<test.count<<std::endl;
--test;
std::cout<<test.count<<std::endl;
return 0;
}

② “[]”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iosteam>
#include<vector>//容器
class Test
{
public:
int operator[](unsigned i)const
{
if(i>0&&i<ivec.size)
return ivec[i];
}
std::vector<int> ivec{1,2,3,4,5,6}
}
int main()
{
Test test;
std::cout<<test[3]<<std::endl;
}

③ “()”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iosteam>
class Test
{
public:
void operator()()const
{
std::cout<<"hello world"<<std::endl;
}
void operator()(const std::string& str)const//可以重载
{
std::cout<<str<<std::endl;
}

}
int main()
{
Test test;
test();//输出hello world
test("abcde")//输出abcde
return 0;
}

④ “<<”,“>>”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iosteam>
class Test
{//记住怎么写
friend std::ostream& operator<< (std::ostream& os,const Test& test);
friend std::istream& operator>> (std::istream& os,Test& test);
}
std::ostream& operator<< (std::ostream& os,const Test& test)
{
os<<test.name<<std::endl;
return os;//返回ostream的引用
}
std::istream& operator>> (std::istream& os,Test& test)
{
is >>test.name;
return is;
}
int main()
{
Test test;
std::cin>>test;
std::cout<<test<<std::endl;
return 0;
}

(2) 二元运算符重载

① “+”,“-”,“*”,“/”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iosteam>
class Test
{
public:
Test opreator+(const Test& test)
{
count +=test.count;
return *this;
}
unsigned count = 0;
std::string name;
}
int main()
{
Test test;
std::cout<<test.count<<std::endl;//输出0
Test test2;
test2.count=20;
Test test3=+test2;
std::test3<<test3.count<<std::endl;//输出20
return 0;
}

② “=”,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iosteam>
class Test
{
Test& opreator=(const Test& test)//使用引用的原因是返回的还是自己
{
if(this==&test)//地址相同
{
return *this;
}
count =test.count;
name=test.name;
}
//Test& opterator= (const Test& test)=default 默认重载
unsigned count = 0;
std::string name;
return *this
}
int main()
{
Test test;
Test test2;
test=test2;//返回是是自己,所以要用引用
return 0;
}

③ “>”,“<”,“==”

1
2
3
4
bool operator< (const Test& test)
{
return count<test.count;
}

至于唯一的三元运算符“?:”,不能重载

(3) 类类型转化运算符:“operator 类型”

(4) 特殊的运算符:new,delete,new[],delete[]

注意:“=”类会默认进行重载,如果不需要可以用“delete关键字进行修饰”。

总结:重载运算符非常重要,C++类中几乎都要定义各种各种的重载运算符。

(*)普通继承及其实现原理

C++面向对象的三大特性:分装,继承,多态。分装就是类的权限管理,很简单,就不讲了。继承这节课讲,继承很重要,有些地方也是需要重点理解的。

**1.**C++继承介绍:C++非继承的类相互是没有关联性的,假设现在需要设计医生,教师,公务员三个类,需要定义很多重复的内容而且相互没有关联,调用也没有规律。如果这还算好,那一个游戏有几千件物品,调用时也要写几千个函数。这太要命了。于是继承能力就应运而生了。

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
#include<iostream>
//class FireSpear麻烦
//{
// std::string name;
// std::string icon;
//};
//class IceSpear
//{
// std::string name;
// std::string icon;
//};
class Spear//父类
{
protected://外界无法访问。但是子类继承到,可以访问
std::string name;
std::string icon;
public:
Spear(const std::string& name_,const std:: string& icon_):name(name_),icon(icon_)
{
std::cout<<"Spear()"<<std::endl;
}
};
class FireSpear: public Spear
{
public:
FireSpear(const std::string& name_,const std:: string& icon_,int i_):Spear(name_,icon_),i(i)
{
std::cout<<"FireSpear()"<<std::endl;
}//先初始化父类的部分,在初始化子类的部分
private:
int i;
};
class IceSpear: public Spear
{

};

**2.**C++继承原理:C++的继承可以理解为在创建子类成员变量之前先创建父类的成员变量,实际上,C语言就是这么模仿出继承功能的。

**3.**C++继承的注意事项。

(1) C++子类对象的构造过程。先调用父类的构造函数,再调用子类的构造函数,也就是说先初始化父类的成员,再初始化子类的成员。

(2) 若父类没有默认的构造函数,子类的构造函数又未调用父类的构造函数,则无法编译。

(3) C++子类对象的析构过程。先调用子类的析构函数,再调用父类的析构函数。

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
#include<iostream>
class Spear
{
public:
Spear(const std::string& name_, const std::string& icon_) :name(name_), icon(icon_)
{
std::cout << "Spear()" << std::endl;
}
~Spear()
{
std::cout << "~Spear()" << std::endl;
}
virtual void openFire()const
{
std::cout << "Spear::openFire" << std::endl;
}
protected:
std::string name;
std::string icon;

};
class FireSpear :public Spear
{
public:
FireSpear(const std::string& name_, const std::string& icon_, int i_) :Spear(name_, icon_), i(i)
{
std::cout << "FireSpear()" << std::endl;
}
~FireSpear()
{
std::cout << "~FireSpear()" << std::endl;
}
private:
int i;
};
class IceSpear: public Spear
{
public:
void openFire()const
{
std::cout << "IceSpear::openFire" << std::endl;
}
};
void openFire(const Spear* pSpear)
{
pSpear->openFire();
delete pSpear;
}
int main()
{
FireSpear fireSpear("fanfan", "sad", 9);
std::cout << "-------------------" << std::endl;
openFire(new FireSpear("fanfan", "love", 10));
std::cout << "-------------------" << std::endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
Spear()
FireSpear()
-------------------
Spear()
FireSpear()
Spear::openFire
~Spear()
-------------------
~FireSpear()
~Spear()

总结:面向对象三大特性的继承就这么简单,很多人觉得类继承很复杂,其实完全不是这样的,只要明白子类在内存上其实就相当于把父类的成员变量放在子类的成员变量前面罢了。构造和析构过程也是为了这个机制而设计的。

(**)虚函数及其实现原理,override 关键字

1.虚函数介绍:

(1) 虚函数就是面向对象的第三大特点:多态。多态非常的重要,它完美解决了上一课设计游戏装备类的问题,我们可以只设计一个函数,函数参数是基类指针,就可以调用子类的功能。比如射击游戏,所有的枪都继承自一个枪的基类,人类只要有一个开枪的函数就可以实现所有枪打出不同的子弹。

(2) 父类指针可以指向子类对象,这个是自然而然的,因为子类对象的内存前面就是父类成员,类型完全匹配。

(3) 当父类指针指向子类对象,且子类重写父类某一函数时。父类指针调用该函数,就会产生以下的可能

该函数为虚函数:父类指针调用的是子类的成员函数。

该函数不是虚函数:父类指针调用的是父类的成员函数。

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
#include<iostream>
class Spear
{
public:
Spear(const std::string& name_, const std::string& icon_) :name(name_), icon(icon_)
{
std::cout << "Spear()" << std::endl;
}
~Spear()
{
std::cout << "~Spear()" << std::endl;
}
virtual void openFire()const//
{
std::cout << "Spear::openFire" << std::endl;
}
protected:
std::string name;
std::string icon;

};
class FireSpear :public Spear
{
public:
FireSpear(const std::string& name_, const std::string& icon_, int i_) :Spear(name_, icon_), i(i)
{
std::cout << "FireSpear()" << std::endl;
}
virtual ~FireSpear()
{
std::cout << "~FireSpear()" << std::endl;
}
virtual void openFire()const override
{
std::cout << "FireSpear::openFire" << std::endl;
}
private:
int i;
};
class IceSpear: public Spear
{
public:
virtual void openFire()const//加了virtual动态绑定
{
std::cout << "IceSpear::openFire" << std::endl;
}
};
void openFire(const Spear* pSpear)
{
pSpear->openFire();
delete pSpear;
}
int main()
{
FireSpear fireSpear("fanfan", "sad", 9);
std::cout << "-------------------" << std::endl;
openFire(new FireSpear("fanfan", "love", 10));
std::cout << "-------------------" << std::endl;
return 0;
}

1
2
3
4
5
6
7
8
9
10
Spear()
FireSpear()
-------------------
Spear()
FireSpear()
FireSpear::openFire
~Spear()
-------------------
~FireSpear()
~Spear()

**2.**虚函数的注意事项:

(1) 子父类的虚函数必须完全相同,为了防止开发人员一不小心将函数写错,于是C++11添加了override关键字。

(2) 父类的析构函数必须为虚函数:当父类对象指向子类对象时,容易使独属于子类的内存泄露。会造成内存泄露的严重问题。

**3.**overide关键字的作用:前面已经说过了,为了防止开发人员将函数名写错了,加入了override关键字。

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
#include<iostream>
class Spear
{
public:
Spear(const std::string& name_, const std::string& icon_) :name(name_), icon(icon_)
{
std::cout << "Spear()" << std::endl;
}
~Spear()
{
std::cout << "~Spear()" << std::endl;
}
virtual void openFire()const//
{
std::cout << "Spear::openFire" << std::endl;
}
protected:
std::string name;
std::string icon;

};
class FireSpear :public Spear
{
public:
FireSpear(const std::string& name_, const std::string& icon_, int i_) :Spear(name_, icon_), i(i)
{
std::cout << "FireSpear()" << std::endl;
}
virtual ~FireSpear()
{
std::cout << "~FireSpear()" << std::endl;
}
virtual void openFire()const override
{
std::cout << "FireSpear::openFire" << std::endl;
}
private:
int i;
};
class IceSpear: public Spear
{
public:
virtual void openFire()const//加了virtual动态绑定
{
std::cout << "IceSpear::openFire" << std::endl;
}
};
void openFire(const Spear* pSpear)
{
pSpear->openFire();
delete pSpear;
}
int main()
{
FireSpear fireSpear("fanfan", "sad", 9);
std::cout << "-------------------" << std::endl;
openFire(new FireSpear("fanfan", "love", 10));
std::cout << "-------------------" << std::endl;
Spear* pSpear = new FireSpear("fanfan", "happy", 11);
std::cout << "-----------------" << std::endl;
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Spear()
FireSpear()
-------------------
Spear()
FireSpear()
FireSpear::openFire
~Spear()
-------------------
Spear()
FireSpear()
-----------------
~FireSpear()
~Spear()

4. 虚函数实现多态的原理介绍

(1) 动态绑定和静态绑定:

① 静态绑定:程序在编译时就已经确定了函数的地址,比如非虚函数就是静态绑定。

② 动态绑定:程序在编译时确定的是程序寻找函数地址的方法,只有在程序运行时才可以真正确定程序的地址,比如虚函数就是动态绑定。

(2) 虚函数是如何实现动态绑定的呢?

① 每个有虚函数的类都会有一个虚函数表,对象其实就是指向虚函数表的指针,编译时编译器只告诉了程序会在运行时查找虚函数表的对应函数。每个类都会有自己的虚函数表,所以当父类指针引用的是子类虚函数表时,自然调用的就是子类的函数。

总结:虚函数是C++类的重要特性之一,很简单,但使用频率非常高,至于如何实现的也要掌握。

静态成员变量与静态函数

**1.**静态成员变量:

(1) Part2的第六节课就讲过C语言的静态成员变量,在编译期就已经在静态变量区明确了地址,所以生命周期为程序从开始运行到结束,作用范围为与普通的成员变量相同。这些对于类的静态成员变量同样适用。

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
class Test
{
public:
static unsigned i;
}
unsigned Test::i=20;//必须在类外进行初始化
int main(){
std::cout<<Test::i<<std::endl;//类名调用
return 0;
}

(2) 类的静态成员变量因为创建在静态变量区,所以直接属于类,也就是我们可以直接通过类名来调用,当然通过对象调用也可以。

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
class Test
{
public:
static unsigned i;
}
unsigned Test::i=20;//必须在类外进行初始化
int main(){
Test test;
std::cout<<test.i<<std::endl;//类对象调用
return 0;
}

**2.**静态成员变量的注意项:

(1) 静态成员变量必须在类外进行初始化,否则会报未定义的错误,不能用构造函数进行初始化。因为静态成员变量在静态变量区,只有一份,而且静态成员变量在编译期就要被创建,成员函数那都是运行期的事情了

**3.**静态成员函数的特点:静态成员函数就是为静态成员变量设计的,就是为了维持封装性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
class Test
{
public:
static unsigned getI() { return i; }
private:
static unsigned i;
};
unsigned Test::i = 20;//必须在类外进行初始化
int main() {
Test test;
std::cout << Test::getI() << std::endl;//类对象调用
return 0;
}

纯虚函数

**1.**纯虚函数介绍:

(1) 还是那个枪械射击的例子,基础的枪类有对应的对象吗?没有。它唯一的作用就是被子类继承。

(2) 基类的openfire函数实现过程有意义吗?没有。它就是用来被重写的。

(3) 所以纯虚函数的语法诞生了,只要将一个虚函数写为纯虚函数,那么该类将被认为无实际意义的类,无法产生对象。纯虚函数也不用去写实际部分。写了编译期也会自动忽略。

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
#include<iostream>
class Spear//父类
{

protected:
std::string name;
std::string icon;
public:
FireSpear(const std::string& name_, const std::string& icon_,int i) Spear(name_,icon_), i(i)
{
std::cout << "Spear()" << std::endl;
}
virtual void openFire()const=0//变成纯虚函数,不需要实现,后边不用写,但是不能把虚构函数和虚构函数忽略
{
std::cout << "Spear::openFire" << std::endl;
}
pirvate:
int i;
};
class FireSpear : public Spear
{
public:
void openFire()const
{
std::cout<<"FireSpear::openfire" << std::endl;
}
};
virtual void openFire(const Spear* pSpear)//加了virtual动态绑定
{
pSpear->openFire();
delete pSpear;
}
int main()
{
openFire(new FireSpear("acd", "sad", 10));
return 0;
}

总结:纯虚函数的特点就是语法简单,却经常使用,必会。

RTTL

**1.**RTTI介绍:

(1) RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够通过基类的指针或引用来检查这些指针或引用所指向的对象的实际派生类。

(2) C++为了支持多态,C++的指针或引用的类型可能与它实际指向对象的类型不相同,这时就需要rtti去判断类的实际类型了,rtti是C++判断指针或引用实际类型的唯一方式。

2.RTTI的使用场景:可能有很多人会疑惑RTTI的作用,所以单独拿出来说一下。

(1) 异常处理:这是RTTI最主要的使用场景,具体作用在异常处理章节会详细讲解。

(2) IO操作:具体作用等到IO章节会详细讲解。

**3.**RTTI的使用方式:RTTI的使用过程就两个函数

(1) typeid函数:typeid函数返回的一个叫做type_info的结构体,该结构体包括了所指向对象的实际信息,其中name()函数就可以返回函数的真实名称。type_info结构体其他函数没什么用.

1
std::cout<<typeid(*指针).name()<<std::endl;

(2) dynamic_cast函数:C++提供的将父类指针转化为子类指针的函数。

1
FirSpear* pFirSpear=dunamic_cast<FireSpear*>(pSpear);//指针和引用可以,转化成功返回对应指针,不成功NONE

重要写法

1
2
3
4
if(std::string(typeid(*pSpear).name())=="class FirSpear")
{
FirSpear* pFirSpear=dunamic_cast<FirSpear*>(pSpear);
}

1.RTTI的注意事项:

当使用typeid函数时,父类和子类必须有虚函数(父类有了虚函数,子类自然会有虚函数),否则类型判断会出错。

RTTI总结:就是C++在运行阶段判断对象实际类型的唯一方式。

多继承

多继承了解一下就可以了。

**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
26
27
28
29
30
31
##include<iostream>
class Base1
{
public:
Base1(int base1I_) :base1I(base1I_) { std::cout << "Base1()" << std::endl; }
protected:
int base1I;
};
class Base2
{
public:
Base2(int base2I_) :base2I(base2I_) { std::cout << "Base2()" << std::endl; }
protected:
int base2I;
};
class Dervied : public Base1, public Base2
{
public:
Dervied(int base1I_, int base2I_, int i_) :Base1(base1I_), Base2(base2I_), i(i_)
{
std::cout << "Derived" << std::endl;
}
private:
int i;
};
int main()
{
Dervied dervied(10, 20, 30);

return 0;
}
1
2
3
Base1()
Base2()
Derived//优先调用子类的参数,但是如果调用父类们相同的参数,就会出错

**2.**多继承的注意点:

(1) 多继承最需要注意的点就是重复继承的问题

(2) 多继承会使整个程序的设计更加复杂,平常不推荐使用。C++语言中用到多继承的地方主要就是借口模式。相较于C++,java直接取消了多继承的功能,添加了借口。

3.多继承的总结:多继承这个语法虽然在某些情况下使代码写起来更加简洁,但会使程序更加复杂难懂,一般来说除了借口模式不推荐使用。

虚继承及其实现原理

**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
26
27
28
29
30
31
32
33
34
#include<iostream>
class TrueBase
{
public:
std::string icon;
int i = 100;
};
class Base1 : virtual public TrueBase//虚继承
{
public:
Base1(int base1I_) :base1I(base1I_) { std::cout << "Base1()" << std::endl; }
int base1I;
};
class Base2 : virtual public TrueBase//虚继承
{
public:
Base2(int base2I_) :base2I(base2I_) { std::cout << "Base2()" << std::endl; }
int base2I;
};
class Dervied :virtual public Base1, virtual public Base2//这里可加可不加,不太理解就加上
{
public:
Dervied(int base1I_, int base2I_, int i_) :Base1(base1I_), Base2(base2I_)
{
std::cout << "Derived" << std::endl;
}
};
int main()
{
Dervied derived(10, 20, 30);
std::cout << derived.i << std::endl;

return 0;
}
1
2
3
4
5
Base1()
Base2()
Derived
100

**2.**虚继承的实现原理介绍:

(1) 使用了虚继承的类会有一个虚继承表,表中存放了父类所有成员变量相对于类的偏移地址。

(2) 按照刚才的代码,B1,B2类同时有一个虚继承表,当C类同时继承B1和B2类时,每继承一个就会用虚继承表进行比对,发现该变量在虚继承表中偏移地址相同,就只会继承一份。

**4.**虚继承的总结:这个语法就是典型的语法简单,但在游戏开发领域经常使用的语法,其它领域使用频率会低很多。

(**)移动构造函数与移动赋值运算符

**1.**对象移动的概念:

(1) 对一个体积比较大的类进行大量的拷贝操作是非常消耗性能的,因此C++11中加入了“对象移动”的操作

(2) 所谓的对象移动,其实就是把该对象占据的内存空间的访问权限转移给另一个对象。比如一块内存原本属于A,在进行“移动语义”后,这块内存就属于B了。

**2.**移动语义为什么可以提高程序运行效率。因为我们的各种操作经常会进行大量的“复制构造”,“赋值运算”操作。这两个操作非常耗费时间。移动构造是直接转移权限,这是不是就快多了。

注意:在进行转移操作后,被转移的对象就不能继续使用了,所以对象移动一般都是对临时对象进行操作(因为临时对象很快就要销毁了)。

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
#include<iostream>
class Test
{
public:
Test() = default;//默认构造函数
Test(const Test& test)
{
if (test.str)
{
str = new char[strlen(test.str) + 1]();//加1不能省略
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
}
Test(Test&& test)//移动构造函数
{
if (test.str)
{
str = test.str;
test.str = nullptr;
}
else
{
str = nullptr;
}
}
Test& operator=(const Test& test)
{
if (this == &test)
{
return *this;
}
if (str) {//是否为空字符串
delete[] str;
str = nullptr;
}
if (test.str)
{
str = new char[strlen(test.str) + 1]();
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
return *this;
}
Test& operator = (Test&& test)
{
if (this == &test)
{
return *this;
}
if (str) {
delete[] str;
str = nullptr;
}
if (test.str)
{
str = test.str;//转移权限
test.str;
}
else
{
str = nullptr;
}
return *this;
}
private:
char* str = nullptr;//规范写法
};
Test makeTest()
{
Test t;
return t;
}
int main() {
Test t = makeTest();
return 0;
}

注意这里的右值引用不能是const的,因为你用右值引用函数参数就算为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,但是你一旦加了const就没法改变该右值引用了。

3. 默认移动构造函数和默认移动赋值运算符

会默认生成移动构造函数和移动赋值运算符的条件:

只有一个类没有定义任何自己版本的拷贝操作(拷贝构造,拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才能为我们合成。

可以移动的意思就是可以就行移动构造,移动赋值。所有的基础类型都是可以移动的,有移动语义的类也是可以移动的。

hhh,周五成功把part3结束,但是移动构造函数还是不太懂,要复习喽

智能指针

智能指针概述

1. 为什么要有智能指针:在Part2的第二节课已经讲过,直接使用new和delete运算符极其容易导致内存泄露,而且非常难以避免。于是人们发明了智能指针这种可以自动回收内存的工具。

2. 智能指针一共就三种:普通的指针可以单独一个指针占用一块内存,也可以多个指针共享一块内存。

(1) 共享型智能指针:shared_ptr,同一块堆内存可以被多个shared_ptr共享。

1
2
3
4
5
6
7
#include<iostream>
int main()
{
int* pi = new int(10);
int* pi2(pi);//pi2和pi共享一段内存
return 0;
}

(2) 独享型智能指针:unique_ptr,同一块堆内存只能被一个unique_ptr拥有。无法拷贝和制造

(3) 弱引用智能指针:weak_ptr,也是一种共享型智能指针,可以视为对共享型智能指针的一种补充

3. (*)智能指针注意事项:

智能指针和裸指针不要混用,接下来的几节课会反复强调这一点。

(*)shared_ptr

1.shared_ptr的工作原理

(1)我们在动态分配内存时,堆上的内存必须通过栈上的内存来寻址。也就是说栈上的指针(堆上的指针也可以指向堆内存,但终究是要通过栈来寻址的)是寻找堆内存的唯一方式。
(2)所以我们可以给堆内存添加一个引用计数,有几个指针指向它,它的引用计数就是几,当引用计数为0时,操作系统会自动释放这块堆内存。
2.Shared_ptr的常用操作

(1)shared_ptr的初始化

①使用new运算符初始化, 一般来说不推荐使用new进行初始化,因为C++标准提供了专门创建shared_ptr的函数“make_shared”,该函数是经过优化的,效率更高。
②使用make_shared函数进行初始化:

1
2
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::shared_ptr<int>sharedI2(sharedI);//允许多个智能指针指向同一块内存

注意:千万不要用裸指针初始化shared_ptr,容易出现内存泄露的问题。

③当然使用复制构造函数初始化也是没有问题的。
代码演示:

(2)shared_ptr的引用计数: 智能指针就是通过引用计数来判断释放堆内存时机的。
use_count()函数可以得到shared_ptr对象的引用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::cout << sharedI.use_count() << std::endl;
std::shared_ptr<int>shareI2(sharedI);
std::cout << sharedI.use_count() << std::endl;
shareI2.reset();
std::cout << sharedI.use_count() << std::endl;
return 0;
}//结果输出121

3.智能指针可以像普通指针那样使用,”share_ptr”早已对各种操作进行了重载,就当它是普通指针就可以了.

**4.Shared_ptr的常用函数 **

(3)unique函数:判断该shared_ptr对象是否独占若独占,返回true。否则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::cout << sharedI.unique() << std::endl;//独占返回1
std::shared_ptr<int>shareI2(sharedI);
std::cout << sharedI.unique() << std::endl;//独占返回0
shareI2.reset();
std::cout << sharedI.unique() << std::endl;//返回1
return 0;
}

(4)reset函数:

①当reset函数有参数时,改变此shared_ptr对象指向的内存。
②当reset函数无参数时,将此shared_ptr对象置空,也就是将对象内存的指针设置为nullptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::cout << sharedI.unique() << std::endl;
sharedI.reset(new int(1000));//原有堆内存被释放,指向新的堆内存,写法不能写成shared_ptr的初始化
std::shared_ptr<int>sharedI2 = std::make_shared<int>(100);
sharedI = sharedI2;
sharedI.reset();//置为空
return 0;
}

(5)get函数,强烈不推荐使用

(6)swap函数:交换两个智能指针所指向的内存

①std命名空间中全局的swap函数

②shared_ptr类提供的swap函数

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::shared_ptr<int>sharedI2 = std::make_shared<int>(1000);
sharedI.swap(sharedI2);//std::swap(sharedI,sharedI2)
std::cout << *sharedI << std::endl;
std::cout << *sharedI2 << std::endl;
return 0;
}

5.关于智能指针创建数组的问题。

1
2
3
std::shared_ptr<int> sharedI(new int[100]());
std::cout<<sharedI.get()[10]<<std::endl;//不能像普通数组一样使用
void myFunc(std::shared_ptr<int> sharedI)//不用写const

6.用智能指针作为参数传递时直接值传递就可以了。shared_ptr的大小为固定的8或16字节(也就是两倍指针的的大小,32位系统指针为4个字节,64位系统指针为8个字节,shared_ptr中就两个指针),所以直接值传递就可以了。

shared_ptr总结:在现代程序中,当想要共享一块堆内存时,优先使用shared_ptr,可以极大的减少内存泄露的问题。

(*)weak_ptr

1. weak_ptr介绍:

(1) 这个智能指针是在C++11的时候引入的标准库,它的出现完全是为了弥补shared_ptr天生有缺陷的问题,其实shared_ptr可以说近乎完美。

(2) 只是通过引用计数实现的方式也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。

1
2
3
4
5
6
7
8
9
10
11
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::shared_ptr<int>sharedI = std::make_shared<int>(100);
std::cout << sharedI.use_count() << std::endl;
std::weak_ptr<int>weakI(sharedI);//weak_ptr指向同一个内存不增加引用计数
std::cout << sharedI.use_count() << std::endl;
return 0;
}

2. shared_ptr的循环引用问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream> 
#include<vld.h>
#include<memory>
class A
{
public:
std::weak_ptr<B> weakB;
};
class B
{
public:
std::shared_ptr<A> sharedA;
};
int main()
{
std::shared_ptr<A>sharedA = std::make_shared<A>();
std::shared_ptr<B>shareB = std::make_shared<B>();
sharedA->weakB = weakB;
weakB->sharedA = sharedA;//如果为两个shared_ptr相互指引导致内存无法释放,所以将其中一个改为weak_ptr
return 0;
}

3. weak_ptr的作用原理:weak_ptr的对象需要绑定到shared_ptr对象上,作用原理是weak_ptr不会改变shared_ptr对象的引用计数。只要shared_ptr对象的引用计数为0,就会释放内存,weak_ptr的对象不会影响释放内存的过程。

总结:weak_ptr使用较少,就是为了处理shared_ptr循环引用问题而设计的。

(*)unique_ptr

1. uniqe_ptr介绍:独占式智能指针,在使用智能指针时,我们一般优先考虑独占式智能指针,因为消耗更小。如果发现内存需要共享,那么再去使用“shared_ptr”。

2 unique_ptr的初始化:和shared_ptr完全类似

(1) 使用new运算符进行初始化

(2) 使用make_unique函数进行初始化

1
2
3
4
5
6
7
8
9
10
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::unique_ptr<int>uniqueI2(new int(100));
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);
return 0;

}

3. unique_ptr的常用操作

(1) unque_ptr禁止复制构造函数,也禁止赋值运算符的重载。否则独占便毫无意义。

(2) unqiue_ptr允许移动构造,移动赋值。移动语义代表之前的对象已经失去了意义,移动操作自然不影响独占的特性。

1
2
3
4
5
6
7
8
9
10
#include<iostream> 
#include<vld.h>
#include<memory>
int main()
{
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);
std::unique_ptr<int>uniqueI2=std::make_unique<int>(200);
uniqueI2=std::move(uniqueI);
return 0;
}

(3) reset函数:

① 不带参数的情况下:释放智能指针的对象,并将智能指针置空。

② 带参数的情况下:释放智能指针的对象,并将智能指针指向新的对象。

和shared_ptr使用方法一样

4. 将unque_ptr的对象转化为shared_ptr对象,当unique_ptr的对象为一个右值时,就可以将该对象转化为shared_ptr的对象。

这个使用的并不多,需要将独占式指针转化为共享式指针常常是因为先前设计失误。

注意:shared_ptr对象无法转化为unique_ptr对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream> 
#include<vld.h>
#include<memory>
void myfunc(std::unique_ptr<int> uniqueI)//需要共享操作时
{
std::shared_ptr<int>sharedI(std::move(uniqueI));//转换
}
int main()
{
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);
std::unique_ptr<int>uniqueI2=std::make_unique<int>(200);
uniqueI2=std::move(uniqueI);
return 0;
}

(**)只能指针适用范围

1. 能使用智能指针就尽量使用智能指针,那么哪些情况属于不能使用智能指针的情况 呢?

有些函数必须使用C语言的指针,这些函数又没有替代,这种情况下,才使用普通的指针,其它情况一律使用智能指针。

必须使用C语言指针的情况包括:

(1) 网络传输函数,比如windows下的send,recv函数,只能使用c语言指针,无法替代.

(2) c语言的文件操作部分。这方面C++已经有了替代品,C++的文件操作完全支持智能指针,所以在做大型项目时,推荐使用C++的文件操作功能。

除了以上两种情况,剩下的均推荐使用智能指针。

2. 我们应该使用哪个智能指针呢?

(1) 优先使用unique_ptr,内存需要共享时再使用shared_ptr。

当使用shared_ptr时,如果出现了循环引用的情况,再去考虑使用weak_ptr

模板与泛型编程

模板介绍,类模板与模板实现原理

1. 模板的重要性:模板是C++最重要的模块之一,很多人对模板的重视不够,这一章一定要好好学,所有课时都是重点。

C++的三大模块,面向过程,面向对象,模板与泛型。面向过程就是C语言,面向对象就是类,现在轮到模板与泛型了。

2. 模板的介绍:

(1) 模板能够实现一些其他语法难以实现的功能,但是理解起来会更加困难,容易导致新手摸不着头脑。

(2) 模板分为类模板和函数模板,函数模板又分为普通函数模板和成员函数模板。

3. 类模板基础:

类模板的写法与使用十分固定

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
#pragma once
template<typename T>
class MyArray
{
using iterator = T*;//定义新的类型
using const_iterator = const T*;
public:
MyArray(size_t count);
~MyArray();
//{ 写在类里面
// if (count)
// {
// data = new T[count]();
// }
// else
// {
// data = nullptr;
// }
//}

//{//类内定义
// if (data)
// {
// delete[] data;
// }
//}
iterator begin()const
{
return data;
}
const_iterator cbegin()const
{
return data;
}

private:
T* data;//在类中复杂情况可以用智能指针,在老版本不支持创建数组
};
template<typename T>//写在类外面,一般都在类外定义
MyArray<T>::MyArray(size_t count)
{
if (count)
{
data = new T[count]();
}
else
{
data = nullptr;
}
}
template<typename T>////类外定义需要先把模板头写上去
MyArray<T>::~MyArray()
{
if (data)
{
delete[] data;
}
}
template<typename T>//begin的类外定义
typename MyArray<T>::iterator MyArray<T>::begin() const//::前面的名称一定是类名或者明明空间
{
return data;
}

注意,这段代码非常有代表性,在下一课补完后,一定要掌握,多看几遍。

4. 模板的实现原理:

模板需要编译两次,在第一次编译时仅仅检查最基本的语法,比如括号是否匹配。等函数真正被调用时,才会真正生成需要的类或函数。所以这直接导致了一个结果,就是不论是模板类还是模板函数,声明与实现都必须放在同一个文件中。因为在程序在编译期就必须知道函数的具体实现过程。如果实现和声明分文件编写,需要在链接时才可以看到函数的具体实现过程,这当然会报错。

于是人们发明了.hpp文件来存放模板这种声明与实现在同一文件的情况。

(*)initializer_list与typename

1.initializer_list的用法

(1) initializer_list介绍:initializer_list其实就是初始化列表,我们可以用初始化列表初始化各种容器,比如“vector”,“数组”。

2.typename的用法

(1) 在定义模板时表示这个一个待定的类型

(2) 在类外表明自定义类型时使用

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#.hpp
#pragma once
#include<type_traits>//萃取技术判断是否是指针
template<typename T>//模板特化
struct get_type
{
using type = T;
};
template<typename T>
struct get_type<T*>
{
using type = T;
};
template<typename T>
class MyArray
{
using iterator = T*;//定义新的类型
using const_iterator = const T*;
public:
MyArray(size_t count);
MyArray(const std::initializer_list<T>& list);
#pragma once
template<typename T>
class MyArray
{
using iterator = T*;//定义新的类型
using const_iterator = const T*;
public:

MyArray(const std::initializer_list<T>& list);
MyArray(std::initializer_list<T>&& list);
MyArray(size_t count);
~MyArray();

iterator begin()const
{
return data;
}
const_iterator cbegin()const
{
return data;
}

private:
T* data;//在类中复杂情况可以用智能指针,在老版本不支持创建数组
};
template<typename T>//写在类外面,一般都在类外定义
MyArray<T>::MyArray(size_t count)
{
if (count)
{
data = new T[count]();
}
else
{
data = nullptr;
}
}
template<typename T>////类外定义需要先把模板头写上去
MyArray<T>::~MyArray()
{
if (data)
{
delete[] data;
}
}
template<typename T>//begin的类外定义
typename MyArray<T>::iterator MyArray<T>::begin() const//::前面的名称一定是类名或者明明空间
{
return data;
}
~MyArray();
iterator begin()const
{
return data;
}
const_iterator cbegin()const
{
return data;
}
T& operator[](unsigned count)const
{
return data[count];//重载
}

private:
std::vector<T> data;
};
template<typename T>//写在类外面,一般都在类外定义
MyArray<T>::MyArray(size_t count)
{
if (count)
{
data = new T[count]();
}
else
{
data = nullptr;
}
}
template<typename T>////类外定义需要先把模板头写上去
MyArray<T>::~MyArray()
{
if (data)
{
delete[] data;
}
}
template<typename T>
MyArray<T>::MyArray(const std::initializer_list<T>& list)
{
if (list.size())
{
unsigned count = 0;
data = new T[list.size()]();
if (std::is_pointer<T>::value)
{
for (auto elem : list)
{
data[count++] = new typename get_type<T>::type(*elem);//相当于两层指针,在删除时,只删除第一层,会出现内存泄漏
}
}
else
{
for (const auto& elem; list)
{
data[count++] = elem;//存在bug,如果存放的是指针,很变成浅复制
}
}
}
else
{
data = nullptr;
}
}
template<typename T>
MyArray<T>::MyArray(std::initializer_list<T>&& list)//右值引用
{
if (list.size())
{
unsigned count = 0;
data = new T[list.size()]();
for (const auto& elem; list)
{
data[count++] = elem;//存在bug,如果存放的是指针,很变成浅复制
};
}
else
{
data = nullptr;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#.cpp
#include<iostream>
#include<vld.h>
#include<vector>
#include"myArray.hpp"

int main()
{
//std::initializer_list<int> iList{ 1,2,3,4,5 };
//std::vector<int> ivec(iList);//左值类型的初始化,右值类型初始化ivec{1,2,3,4,5}
int i1 = 10;
int i2 = 20;
int i3 = 30;
int i4 = 40;
std::initializer_list<int*> iList{ &i1,&i2,&i3,&i4 };
MyArray<int*> arrayPi(iList);
for (unsigned i = 0; i < 4; ++i)
{
std::cout << arrayPi[i] << std::endl;
}
return 0;
}

在C++的早期版本,为了减少关键字数量,用class来表示模板的参数,但是后来因为第二个原因,不得不引入typename关键字。

(*)函数模板,成员函数模板

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
26
27
#include<iostream>
#include<vld.h>
#include<vector>
#include<memory>
#include"myArray.hpp"
namespace mystd
{
template<typename iter_type ,typename func_type>//普通函数模板
void for_each(iter_type first, iter_type last, func_type func)
{
for (auto iter = first; iter != last; ++iter)
{
func(*iter);
}
}
}
int main() {
std::vector<int> ivec{ 1,2,3,4,5 };
mystd::for_each(ivec.begin(), ivec.end(), [](int& elem) {
++elem;
});
for (int elem : ivec)
{
std::cout << elem << std::endl;
}
return 0;
}

在现代C++中,函数模板一直普遍使用,一定要掌握。

2.成员函数模板

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
#include<iostream>
#include<vld.h>
#include<vector>
#include<memory>
#include"myArray.hpp"

namespace mystd
{
template<typename iter_type, typename func_type>
void for_each(iter_type first, iter_type last, func_type func)
{
for (auto iter = first; iter != last; ++iter)
{
func(*iter);
}
}
template<typename T>//成员函数模板
class MyVector
{
public:
template<typename T2>
void outPut(const T2& elem);

};
template<typename T>
template<typename T2>
void MyVector<T>::outPut(const T2& elem)
{
std::cout << elem << std::endl;
}
}


int main() {
mystd::MyVector<int> myVec;
myVec.outPut<int>(20);
return 0;
}

成员函数模板使用情况也不少,需要掌握的

(*)默认模板参数

默认模板参数:

(1) 默认模板参数是一个经常使用的特性,比如在定义vector对象时,我们就可以使用 默认分配器。

(2) 模板参数就和普通函数的默认参数一样,一旦一个参数有了默认参数,它之后的参 数都必须有默认参数

(3) 函数模板使用默认模板参数

(2) 类模板使用模板参数

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
#include<iostream>
#include<vld.h>
#include<vector>
#include<functional>
#include<algorithm>
#include<memory>
#include"myArray.hpp"

namespace mystd
{
using void_int_func_type = std::function<void(int&)>;
template<typename iter_type, typename func_type=void_int_func_type>//函数模板使用模板参数,typename这里有默认值,下边都要有默认值
void for_each(iter_type first, iter_type last, func_type func = [](int& elem){
++elem;
})
{
for (auto iter = first; iter != last; ++iter)
{
func(*iter);
}
}
template<typename T, typename allocator = std::allocator<T>>//类模板使用默认模板参数
class MyVector
{
public:
template<typename T2>
void outPut(const T2& elem);

};
template<typename T,typename allocator>
template<typename T2>
void MyVector<T,allocator>::outPut(const T2& elem)
{
std::cout << elem << std::endl;
}
}


int main()
{
std::vector<int> ivec{ 1,2,3,4,5 };
mystd::for_each(ivec.begin(), ivec.end());
for (auto elem : ivec)
{
std::cout << elem << std::endl;
};
return 0;
}

几乎stl库都在使用模板参数

(*)模板的重载,全特化和偏特化

1.模板的重载

(1) 函数模板是可以重载的(类模板不能被重载),通过重载可以应对更加复杂的情况。比如在处理char和string对象时,虽然都可以代表字符串,但char在复制时直接拷贝内存效率明显更高,string就不得不依次调用构造函数了。所以在一些比较最求效率的程序中对不同的类型进行不同的处理还是非常有意义的。

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
#include<iostream>

template<typename T>
void test(const T& parm)
{
std::cout << "void test(const T& parm)" << std::endl;
}
template<typename T>
void test(T* parm)//若加上const,test(&10)则会调用第一个
{
std::cout << "void test(T* parm)" << std::endl;
}

void test(double parm)
{
std::cout << "void test(double parm)" << std::endl;
}
int main()
{
test(100);
int i = 100;
test(&i);
test(2.2);
return 0;
};
1
2
3
void test(const T& parm)
void test(T* parm)
void test(double parm)

其实函数模板的重载和普通函数的重载没有什么区别。

2.模板的特化

(1) 模板特化的意义:函数模板可以重载以应对更加精细的情况。类模板不能重载,但可以特化来实现类似的功能。

(2) 模板的特化也分为两种,全特化和偏特化。模板的全特化:就是指模板的实参列表与与相应的模板参数列表一一对应。

(3) 模板的偏特化:偏特化就是介于普通模板和全特化之间,只存在部分类型明确化,而非将模板唯一化。

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
#include<iostream>

template<typename T1,typename T2>
class Test
{
public:
Test() {
std::cout << "common template" << std::endl;
}
};
template<typename T1, typename T2>
class Test<T1*, T2*>
{
public:
Test() {
std::cout << "point semi-template" < , std::endl;
}
};
template<typename T2>
class Test<int, T2>//只写一部分叫偏特化
{
public:
Test() {
std::cout << "int ssssemi-special" << std::endl;
}
};
template<>
class Test<int, int>
{
public:
Test() {
std::cout << "int,int complete special" << std::endl;
}
};
int main()
{
Test<int*, int*> test;
return 0;
};

(4) 其实对于函数模板来说,特化与重载可以理解为一个东西。

总结:函数模板的重载,类模板的特化。还是比较重要的知识点,应当掌握,在一些比较复杂的程序中,模板重载与特化是经常使用的。


现代C++学习笔记
http://example.com/2023/03/05/现代C++学习笔记/
作者
fan fan
发布于
2023年3月5日
许可协议