C++学习笔记:

名称空间std

image-20221113132517924

<math.h>新式为

<string.h>新式为

如果使用iostream,而不是iostream.h,即当头文件中没有扩展名h时,iostream中定义的用于输出的cout变量实际是std::cout,而endl实际上是std::endl.因此,可以省略编译指令using,用下述方式进行编码:

std::cout<<" xxxxxx";

std::cout<<std::endl;

但如果使用using编译指令:using namespace std;

便可以直接使用cin和cout…,而不必加上std::前缀

控制符endl

image-20221113133422849

C++中的旧版换行采用的是"\n

image-20221113133633328

cin和cout

(1).image-20221113135231199

(2).

输出可以拼接:

image-20221113140118344**

每次读取一行字符串(string)输入

image-20221113193250853

image-20221113194203451

image-20221113194311275

总结:

1.cin(>>)

虽然可以使用 cin 和 >> 来输入字符串,当 cin 读取数据时,一旦它接触到第一个非空格字符即开始阅读,当它读取到下一个空白字符时,它将停止读取。

可以输入 “Mark” 或 “Twain”,但不能输入 “Mark (停止读取) Twain”,
因为 cin 不能输入包含嵌入空格的字符串。

2.cin.get(char ch)/(array_name,size)

无参数时,读入一个字符,包括换行符,常用来处理输入缓冲区中的换行符。

有参数时,从缓冲区读取数据,到达行尾或size-1个字符**(剩下的空间储存在结尾添加的空字符)**后结束读取(超过规定字符数不会出现错误,会直接截断),不会对换行符进行处理,将其留在缓冲区

3.cin.getline(array_name,size)

从缓冲区读取数据,到达行尾或size-1**(剩下的空间储存在结尾添加的空字符)**个字符结束读取(超过规定的字符数会出现错误,中断),会读取换行符将其替换为空字符,并且丢弃。

**4.getline(cin,array_name) **/ std::getline(std::cin,array_name)

例如:getline(cin,str);

从缓冲区中读取数据,遇到换行符时将其替换为空字符,并且丢弃。

补:

(1)cin.getline(char*s,streamsize n,char delim)

所需的头文件为(这里的参数char s是输入的字符串变量, n是输入字符串的字符个数(第n个补’\0’), delim是输入终止条件,即遇到delim所代表的字符就终止输入,正常使用时 char delim可以省略,c++语言默认为’\0’) 例 : cin.getline(name,20,‘C’)或cin.getline(name,20)

(2)getline(istream& is, string& str, char delim)

所需的头文件为(s是标准输入流函数, str是用来存字符的变量名, delim是结束标志,作用与cin.getline()里的相同)例: getline(cin,str,‘A’)

==注: getline()是string流的函数,只能用于string类型(不能用于输入char*类型)的输入操作.==

==cin.getline()是std流的函数,只能用于char*类型的输入操作(不能用于string类型输入)。== char*为数组

当你定义了一个char*类型变量,只能用cin/cin.getline()输入。!!!!!

**(3).**在使用getline读入一整行时,
若是前面是使用getchar()、cin这类读入了一个字母,
但是不会读入后续换行\n符号或者空格的输入时,再接getline()就容易出现问题。

这是因为输入数字之后,敲回车,产生的换行符仍然滞留在输入流了,
接着就被getline(cin,s)给读进去了,
此时的s=“\n”,所以实际上s只是读入了一个换行符\n。

而若是前面使用getline(),再又用getline()进行读入,此时不会发生问题。
getline()中读入结束的回车后,结束符不放入缓存区,会将读入的\n直接去除,
下一个输入前,缓冲区为空,并不会因为回车留下\n。

而cin的结束后,以及getchar()此类的读入结束后,按下回车或者使用空格读入下一个,
此时按下的回车或空格会还在缓存区,继续用getline()就会出现前面所提到的情况。
那么如何解决前面用了cin、getchar()后的输入呢?
可以直接在cin和getchar()后使用一个getchar()吃掉接下来的换行.

string类(头文件"string")

1.image-20221113202153066

2.

image-20221113202108337

3.数组和string类的不同点:

**(1)**在数组中,不能将一个数组赋给另一个数组,但可以将一个string对象赋给另一个string对象。

**(2)**可以使用+让两个string对象合并起来,还可以使用+=将字符串附加到string对象的末尾。

同时可以对字符串实现运算符(==,=),可以直接比较

image-20221113202543882

4.用getline将输入读取到string对象

image-20221113202709793

补:获取字符串长度

1.length()函数
直接获取字符串长度,包括空格在内

表示方法: **str.length()**即可表示str字符串的长度

2.strlen()函数
需要添加头文件<string.h>,而且参数只能是char数组(不能是string类)
而且结尾必须是\0(即字符数组不能满,因为满了结尾就不是\0,会接着向下检索到\0为止)
啊哦char c[6]{“kunkun”}; 这样是错误的嗷 直接没法进行编译,编译器直接帮我们解决问题2。

表示方法: strlen(数组名)

3.size()

需要添加<string.h>头文件,用法类似于length()。
size()表示的是string这个容器中的元素个数。(还可以获取vector类型的长度)
如果使用过std::vector之类的容器的话,可以把string看做是一个vector(这里只是举例,并不能等价), char就是这个容器的元素类型。那么size()表示的就是这个vector(容器)中char的个数。
表示方法:

**str.size()**即可表示str字符串的长度(不包含’\0’)

数组的替代品

具体见CSDN

模板类vector

  • vector是向量类型,可以容纳许多类型的数据,因此也被称为容器

  • (可以理解为动态数组,是封装好了的类)

  • 进行vector操作前应添加头文件#include <vector>

  • .2 vector初始化:
    方式1.

    定义具有10个整型元素的向量(尖括号为元素类型名,它可以是任何合法的数据类型),不具有初值,其值不确定

    1
    vector<int>a(10);

    方式2.

    定义具有10个整型元素的向量,且给出的每个元素初值为1

    1
    vector<int>a(10,1);

    方式3.

    用向量b给向量a赋值,a的值完全等价于b的值

    1
    vector<int>a(b);

    方式4.

    将向量b中从0-2(共三个)的元素赋值给a,a的类型为int型

    1
    vector<int>a(b.begin(),b.begin+3);

    方式5.

    //从数组中获得初值
    int b[7]={1,2,3,4,5,6,7};

    1
    vector<int> a(b,b+7);

    1.3 vector对象的常用内置函数使用(举例说明)

    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
    #include<vector>
    vector<int> a,b;
    //b为向量,将b的0-2个元素赋值给向量a
    a.assign(b.begin(),b.begin()+3);
    //a含有4个值为2的元素
    a.assign(4,2);
    //返回a的最后一个元素
    a.back();
    //返回a的第一个元素
    a.front();
    //返回a的第i元素,当且仅当a存在
    a[i];
    //清空a中的元素
    a.clear();
    //判断a是否为空,空则返回true,非空则返回false
    a.empty();
    //删除a向量的最后一个元素
    a.pop_back();
    //删除a中第一个(从第0个算起)到第二个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+3(不包括它)结束
    a.erase(a.begin()+1,a.begin()+3);
    //在a的最后一个向量后插入一个元素,其值为5
    a.push_back(5);
    //在a的第一个元素(从第0个算起)位置插入数值5,
    a.insert(a.begin()+1,5);
    //在a的第一个元素(从第0个算起)位置插入3个数,其值都为5
    a.insert(a.begin()+1,3,5);
    //b为数组,在a的第一个元素(从第0个元素算起)的位置插入b的第三个元素到第5个元素(不包括b+6)
    a.insert(a.begin()+1,b+3,b+6);
    //返回a中元素的个数
    a.size();
    //返回a在内存中总共可以容纳的元素个数
    a.capacity();
    //将a的现有元素个数调整至10个,多则删,少则补,其值随机
    a.resize(10);
    //将a的现有元素个数调整至10个,多则删,少则补,其值为2
    a.resize(10,2);
    //将a的容量扩充至100,
    a.reserve(100);
    //b为向量,将a中的元素和b中的元素整体交换
    a.swap(b);
    //b为向量,向量的比较操作还有 != >= > <= <
    a==b;


size()

C++中,在获取字符串长度时,size()函数与length()函数作用相同。

1
2
3
4
string str;
cin>>str;
cout<<str.size()<<endl;
cout<<str.length()<<endl;

size()函数以及length()函数都用于计算字符串(string)长度,不能用char*作为参数。除此之外,size()函数还可以获取vector类型的长度。

size()函数返回值为unsigned int 类型为正数

注意 让其作为返回值赋给变量时,变量类型要为
unsigned int 或 size_t型

补:size_t

size_t 是一些C/C++标准在stddef.h中定义的,size_t 类型表示C中任何对象所能达到的最大长度,它是无符号整数。

32位上的定义: 等价于 unsigned int

64位上的定义: 等价于 unsigned long

empty()

C++中empty()作为判断容器是否为空的函数

用法为 对象名.empty() 例: s.empty()

如果对象为空就返回1(ture),

不为空返回0(false)

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

int main() {
std::string s = ""

if (s.empty()) {
cout << “字符串为空”;
}
else {
cout << "字符串不为空";
}
}

swap() swap(交换)

具体见CSDN

标准库的C ++中swap()函数是一个在两个相同类型的给定变量之间直接交换值的函数。元素个数不相等也可以进行交换

用法:

1
std::swap(a,b);

insert() insert(插入)

对象.insert()

几种用法: 下标(索引)都是默认从第0个位置开始(第0个位置,第1个位置)

1、在第index位置插入count个字符c---->str.insert(index,count,c);

1
2
3
4
string str = "012356789";
cout << "插入前的字符串---->" << str << endl;012356789
str.insert(4,1,'4');
cout << "插入后的字符串---->" << str << endl; 0123456789

2、在第index位置插入一个常量字符串---->str.insert(index,str);

1
2
3
4
string str = "0156789";
cout << "插入前的字符串----->" << str << endl; 0156789
str.insert(2,"234");
cout << "插入后的字符串----->" << str << endl; 0123456789

3、第index位置插入常量字符串str中的count个字符---->str.insert(index,str,count);

1
2
3
4
string str = "01236789";
cout << "插入前的字符串----->" << str << endl;
str.insert(4,"456789",2);
cout << "插入后的字符串----->" << str << endl; 0123456789

4、第index位置插入常量str---->str.insert(index,str);

1
2
3
4
5
string str = "01236789";
cout << "插入前的字符串----->" << str << endl;
str.insert(4,"45");
cout << "插入后的字符串----->" << str << endl; 0123456789

5、第index位置插入常量str的从index_str开始的count个字符---->str.insert(index,str,index_str,count);

1
2
3
4
string str = "01236789";
cout << "插入前的字符串----->" << str << endl;
str.insert(4,"2345678",2,2);
cout << "插入后的字符串----->" << str << endl; 0123456789

6、index位置插入常量str从index_str开始以后的字符串---->str.insert(index,str,index_str,string::npos);

1
2
3
4
string str = "01236789";
cout << "插入前的字符串----->" << str << endl;
str.insert(4,"012345",4,string::npos);
cout << "插入后的字符串----->" << str << endl; 0123456789

NULL和nullptr

c中用NULL表示空指针

但在c++中用nullptr表示空指针,把NULL当作0来使用

具体的见CSDN

对类的定义:

首先用class+标记名对类进行定义1.private部分,2.public部分

(1)private中的数据为隐藏数据(通常是变量),只能通过public中的成员函数对其进行访问,外部没有访问权力。

(2)public中通常是成员函数,可以在成员函数中访问该类的private中的数据,通常只在里面声明函数原型,在其他地方定义(此处用的链表,链表的下一个结点)

**注:**访问成员函数要作用域解析符 : : ,格式为 : 类标记名+ : : + 函数名 尤其是在定义时 例:

1
2
3
4
5
6
void Input(){//定义一个全新的函数Input,不是成员函数

}
void Classroom::Input(){//对成员函数进行定义

}

此处用了友元类,具体见笔记

类对象(变量)的创建

标记名 + 对象名 例:

1
2
3
4
5
6
7
8
9
10
11
12
class Classroom{
private:

public:
void Input();
}
Classroom a//创建一个Classroom类的对象a
int main()
{
Classroom::Input();//错误写法
a.Input()//正确写法
}

重点!! 注意:在调用成员函数之前,必须创建对象,不能直接像定义成员函数那样调用成员函数. 如上方

调用成员函数的方式是通过 点关系符 ” . “

!!!! 类访问==成员函数==时用"."访问,并且如果没有参数访问的时候必须带上后面的括号,否则认为访问的是变量;

格式为: 对象名 . 函数名()

例如: a.Input();

下为类对象(变量)在学生管理程序中使用:

image-20221120220212245

new和delete

该处以链表+类为例:

![image-20221120200903909](E:\!!!!Markdown\C++.assets\image-20221120200903909.png![image-20221120202508583](E:\!!!!Markdown\C++.assets\image-20221120202508583.pngimage-20221120202614633

1.New

(1)用malloc分配内存时需要头文件cstdlib,但是new 不需要引用新的头文件。

(2)new为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配的内存

通用格式如下:typename * pointer_name = new typeName

1
2
int *p = new int(10);//申请了一个初值为10的整型数据,括号中为初始化的值
int *arr = new int[10];//申请了能存放10个整型数据元素的数组,其首地址为arr

注意:

*int p=new int 此时解引用p的值将会是一个随机值,未初始化。

*int p=new int() 此时括号里为空,解引用p的值将自动初始化为0

此处文件中current 是一个指针 ,Student是类标记,new + 类型名 返回的值是一个地址

(3)new 从被称为自由储存区的内存区域分配内存,除了主动释放外不会被回收。而局部变量通常储存在的内存区域中

2.Delete

delete 用来释放new分配的内存

通常 delete + 指针名 即可(注意:delete不一定使用用于new的指针,而是用于new的地址)

1
2
delete p;
delete[] arr;//注意要删除数组时,需要加[],以表示arr为数组。

使用new 和 delete 应该遵守以下规则:

1.不要使用delete来释放不是new分配的内存。

2.不要使用delete来释放同一个内存块两次。

3.如果使用new [ ]为数组分配内存,则应该使用delete [ ]来释放

4.如果使用new为一个实体分配内存,则应该使用delete(没有方括号)来释放。

5.对空指针使用delete是安全的。

C++中文件的输入输出

写入到文本文件中

==要求==:

1.包含头文件iostream,

包含头文件fstream

iostream: 头文件中定义了一个处理输出的ostream类

fstream:头文件定义了一个用于处理输出的ofstream类

2.声明一个或多个ofstreamoutput fstream)的变量(对象),并且命名,遵守常用的命名的规则,通常取名为outFile

**3.**必须指明名称空间std,为引用ofstream,必须使用编译指令using或者前缀的std::

**4.**将ofstream对象与文件关联起来。为此,方法之一是使用open()方法

**5.**使用完文件后,应使用close()将其关闭

**6.**可结合ofstream对象 和运算符<<来输出各种类型的数据。

**总结:**文件的输出主要步骤如下:

1.包含头文件fstream

2.创建一个ofstream(output fstream)对象(通常取名为outFile

3.将该ofstream 对象同一个文件关联起来。

**4.向cout那样使用ofstream对象(通常outFile)**重点:cout在屏幕上输出,而outFile是在文件中输出(写入)

例子见下方

从文本文件中读取数据

==要求==:

1.包含头文件iostream,包含头文件fstream

iostream: 头文件中定义了一个处理输出的istream类

fstream:头文件定义了一个用于处理输入的ifstream类

2.声明一个或多个ifstreaminput fstream)的变量(对象),并且命名,遵守常用的命名的规则,通常取名为inFile

**3.**必须指明名称空间std,为引用ifstream,必须使用编译指令using或者前缀的std::

**4.**将ifstream对象与文件关联起来。为此,方法之一是使用open()方法

**5.**使用完文件后,应使用close()将其关闭

**6.**可结合ifstream对象 和运算符<<来输出各种类型的数据。

检查文件

格式为: 对象名.isopen() 例: inFile.is_open()

如果文件成功被打开,返回true;如果文件没有被打开,因此表达式 !inFile.is_open() 将为true

通常使用下方代码判断是否打开成功

1
2
3
4
5
if(!inFile.is_open())
{
cout<<"文件打开失败";
exit(1);
}

exit(0)程序正常结束

exit(1)程序异常结束

exit()使用需要用到头文件==cstdlib==

文件读取结尾

eof在遇到EOF(文件结束标志)时返回ture ,否则返回false

通常和while循环一起用作文件读取结束,!inFile.eof在文件结束前为真,结束为假(退出循环)

具体格式为: while( !inFile.eof())

文件的打开和关闭

**1.对象名.open(“文件名”) **(一个参数)例:outFile.open(“student.txt”) 如果文件不存在,将会自动创建一个相同名字的文件,如果文件存在,将会打开该文件,首先截断该文件,将其长度截短到0,丢弃原有的内容,然后将新的输入加入到该文件中

2.对象名.open(“文件名”,打开方式) (两个参数)例如 : **outFile.open(“student.txt”,std::ios::in)**只读模式 见下方

格式:对象名.close() 例: outFile.close()

注意方法close(),不需要使用文件名作为参数,因为outFile已经同特定的文件关联起来,如果忘记关闭文件,程序正常终止的时候将自动关闭它。

下面时学生管理程序中的例子:

读取数据到文件中(output输出数据到文件):

image-20221120212115308

从该文件中读取数据(input从文件中输入数据到外):

image-20221120212136110

从文件中读取数据时,也需要用到new分配新的内存,与Input()函数在输入数据时相同

inFile 对应的c中的fscanf(从文件中读取数据)

outFile对应的时c中的fprintf(写入数据到文件)

构造函数和析构函数

构造函数

  • 按参数种类分:无参构造函数、有参构造函数、有默认参构造函数
  • 按类型分为:普通构造函数、拷贝构造函数(赋值构造函数)

C++构造函数的各种用法全面解析(C++初学面向对象编程)_c++ 构造函数-CSDN博客

运算符重载

运算符重载的概念和原理

如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。

有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。例如,复数是可以进行四则运算的,两个复数对象相加如果能直接用+运算符完成,不是很直观和简洁吗?

利用 C++ 提供的“运算符重载”机制,赋予运算符新的功能,就能解决用+将两个复数对象相加这样的问题。

运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。

运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:

1
2
3
4
5
6
7
8
返回值类型  operator  运算符(形参表)
{
....
}

//运算符可以是+、-、*、/等,必须是有效的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
/*一个简单的重载+运算符*/

#include <iostream>
class Test{

private:
int testData;


public:
inline int getData()
{
return testData;
}

inline void setData(int data)
{
this->testData = data;
}

Test operator+(const Test& t)
{
Test test;
test.testData = testData+t.testData;

return test;
}

};




int main() {

Test t1,t2;
Test t3,t4;

t1.setData(5);
t2.setData(15);

t3 = t1+t2;

t4 = t1.operator+(t3);
//t4 = t1+t2+t3 这是允许的,得出的结果相同
std::cout << "t1:" << t1.getData() << std::endl;
std::cout << "t2:" << t2.getData() << std::endl;
std::cout << "t3:" << t3.getData() << std::endl;
std::cout << "t4:" << t4.getData() << std::endl;
}

// 注意:重载后的+号允许大于两个对象相加,如t4 = t1+ t2 + t3

该函数有两种调用方式

  • 通过对象调用方法来调用,如上边的t4
  • 直接通过重载的符号调用,如上方t1+t2

运算符重载限制:

运算符重载规则,即允许重载的运算符,不允许重载的运算符见:

【⑤C++ | 运算符重载】意义 | 限制 | 方法 | 规则 | 特殊运算符重载 | 应用场景-CSDN博客

注:在 C++ 中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。 类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。

类的继承

继承的定义

定义:

继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称派生类(或子类),被继承的类称基类(或父类)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用。

继承:is-a 关系

因为派生类可以在基类上添加新特性,所以这种关系成为is-a-kind-of(是一种)关系更加准确,通常术语是is-a关系。例如:香蕉是一种水果

同时在指针指向的对象上面也有讲究:

  • 可以将基类指针指向派生类对象(多态性体现)
  • 不能将派生类指针指向基类对象(不允许这样做)
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 Fruit
{
private:

public:
};

class Banana:public Fruit
{
private:

public:
};

int main()
{
Fruit *pFruit1 = new Fruit();//正确
Fruit *pBanana = new Banana();//正确,满足香蕉一种水果,水果包含香蕉的关系
/*
使用基类指针引用派生类对象的能力允许多态性。在这种情况下,你可以通过基类指针调用基类的函数或访问基类的成员,而在运行时,将调用正确的派生类方法。
*/

Banana* pFruit2 = new Fruit();//错误,水果不是一种香蕉,香蕉不包含水果,不允许这样做
Banana* pBanana1 =new Banana();//正确

return 0;
}

继承的格式

1
2
3
4
class 新类的名字: 继承方式 继承类的名字
{

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

using namespace std;

/*一个简单的例子*/
class people
{
public:
string name;
int age{};

people() = default;
people(string name,int age)
{
this->name = name;
this->age = age;
}

};

class student:public people
{
private:
int schoolnum;
public:

student(string name,int age,int schoolNum):people(name,age)//使用成员初始化列表语法,会先调用基类的构造函数
{
this->schoolnum = schoolNum;
}
/*上面的构造函数等价于
student(string name,int age,int schoolnum)
{
this->name = name;
this->age = age;
this->schoolnum = schoolnum;
}
这两种方法都可以
*/

inline void print()
{
cout << name << endl << age << endl << schoolnum << endl;
}


};

int main()
{
student s("keqiudi",18,2022124018);
s.print();
return 0;
}

在继承的时候可以使用类名加上作用域解析符(:)来调用基类的方法,通常在私有继承中使用,第二种便是使用this指针调用继承过来的基类的方法

更多详细内容学习:

c++:继承(超详解)

继承的总结:

  1. 基类private成员无论以什么方式继承到派生类中都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:public>protected>private)。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,但最好显式地写出继承方式。

多态公有继承

简单来说就是一个方法的行文随上下文而异,有两种重要机制可用于实现多态的公有继承

  1. 在派生类中重新定义基类的方法
  2. 使用虚方法

虚方法

  • 关键词:virtual
  • 在基类中将派生类会重新定义的方法声明为虚方法。使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法,而未使用时程序将仅仅根据引用或指针的类型来选择方法,与多态性紧密相关。
  • virtual关键词只用于类声明的方法原型中,而不用于类方法实现中
  • 派生类中覆盖的方法后要加上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
//brass.h
#ifndef BRASS_H
#define BRASS_H
#include <string>
using namespace std;

class Brass
{
private:
string fullName; //客户姓名
long acctNum; //账号
double balance; //当前结余

public:
Brass(const string &s = "Nullbody", long an = -1, double bal = 0.0); //创建账户
void Deposit(double amt); //存款
virtual void Withdraw(double amt); //取款
double Balance() const; //显示账户信息
virtual void ViewAcct() const; //显示
virtual ~Brass() {}
};

class BrassPlus : public Brass
{
private:
double maxLoan; //透支上限
double rate; //透支贷款利率
double owesBank; //当前的透支总额

public:
BrassPlus(const string &s = "Nullbody", long an = -1, double bal = 0.0, double m1 = 500, double r = 0.11125);
BrassPlus(const Brass &ba, double m1 = 500, double r = 0.11125);
void ViewAcct() override; //覆盖基类实现,函数重载
void Withdraw(double amt) override; //覆盖基类实现,函数重载
void ResetMax(double m) { maxLoan = m; } //透支上限
void ResetRate(double r) { rate = r; } //透支贷款利率
void ResetOwes() { owesBank = 0; } //当前透支总额
};
#endif

设计的Brass基类指针既可以指向Brass对象,也可以指向BrassPlus对象,因此可以使用一个数组来表示多种类型的对象,这就是多态性。下面在一个数组中可以很清楚的看清virtual的作用:

1
2
3
4
5
6
7
8
9
10
const int CLIENTS = 2;
Brass *clients[CLIENTS];//Brass类型的指针
clients[0] = new Brass("Test0", 1234, 1234.56);//指向Brass类型对象
clients[1] = new BrassPlus("Test1", 5678, 5678.91);//指向BrassPlus类型对象

for (int i = 0;i < CLIENTS;++i)
{
clients[i]->ViewAcct();
}

多态性说明

  • 假如ViewAcct()是使用关键字virtual声明

如果数组成员(指针)指向的是Brass对象,则调用的是Brass::ViewAcct()。
如果数组成员(指针)指向的是BrassPlus对象,则调用的是BrassPlus::ViewAcct()。

  • 假如ViewAcct()不是虚方法

则在任何情况下都将调用Brass::ViewAcct()。

虚析构函数

基类要声明一个虚析构函数,为了确保释放派生类对象时,按正确的顺序调用析构函数

1
2
3
4
5
6
7
8
9
class Brass
{
private:
string fullName; //客户姓名
long acctNum; //账号
double balance; //当前结余
public:
virtual ~Brass() {} //这是虚析构函数
};

为何使用虚析构函数

  • 如果析构函数不是虚方法,则将只调用对应于指针类型的析构函数。对于Brass * 指针将只调用Brass基类的析构函数,即使Brass * 指针指向的是BrassPlus对象。
  • 如果析构函数是虚方法,将调用相应的指向对象类型的析构函数。即如果指针指向的是Brass对象,将调用Brass对象的析构函数,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。
  • 如果BrassPlus包含一个执行某些操作的析构函数,则Brass必须有一个虚析构函数,即使Brass的析构函数不执行任何操作。

静态联编和动态联编

静态联编:静态联编是指联编工作在编译阶段完成的,联编过程是在程序运行之前完成。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差,也因此作为C++默认联编选择。

动态联编:动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时是虚函数的实现。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低

动态联编规定:只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)

  • 编译器对非虚方法使用静态联编
  • 编译器对虚方法使用动态联编

实现动态联编需要同时满足以下三个条件:

① 必须把动态联编的行为定义为类的虚函数。

② 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来。

③ 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数

总结

大多数情况下,动态联编很好,因为他让程序能够选择特定类型设计的方法。虚函数是实现多态的基础,是实现动态联编的必要条件之一。动态联编要靠虚函数来实现,虚函数要靠动态联编的支持。两者相辅相成,缺一不可。

初始化列表

概念

当在C++中定义类的构造函数时,可以使用初始化列表来初始化类的成员变量。初始化列表是在构造函数的参数列表后面使用冒号(:)分隔开来的一组初始化语句,多个参数初始化使用逗号隔开,用于初始化类的成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
/*初始化列表写法*/
A(int value,double values):data(value),datas(values)//这里的冒号及冒号右边的部分就是构造函数的初始化列表,逗号隔开
{

}
/*函数体内写法*/
A(int value,double values)
{
data = value;
double = datas;
}

private:
int data;
double datas;
};

注意

  • 对于继承的对象,构造函数在成员初始化列表中使用基类名来调用特定的基类构造函数。
  • 对于成员对象,构造函数则使用成员名。

什么时候必须使用

  1. 在成员变量为引用类型时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
private:
int &data;

public:
A(int value):data(value)//正确
{
}

A(int value)
{
data = value;//会报错引用类型不允许这样做,只能使用初始化列表(上面的方式初始化)
}
};
  1. 在成员变量为const时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A 
{
public:
A(int value):data(value)
{
}

A(int value)
{
data = value;//会报错const类型不允许这样做,只能使用初始化列表(上面的方式初始化)
}

private:
const int data;
};

建议使用的地方

  • 初始化基类部分

在派生类构造函数中使用初始化列表法初始基类的变量,可以避免在函数体中进行额外的赋值操作提高效率,同时提高可读性

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 A {
public:
/*对于成员对象使用成员名初始化列表*/
explicit A(int value):data(value)//这里的冒号及冒号右边的部分就是构造函数的初始化列表,逗号隔开
{

}

protected:
int data;
};

class B:public A
{
private:
double datas;
public:
/*对于继承的对象,使用基类名初始化列表,调用基类构造函数初始化基类部分*/
B(int value,double values):A(value)
{
datas = values; //正常的在函数体内初始化派生类部分,更清晰表明继承关系,这个地方也可以在上面使用初始化列表方式为datas赋值
}
};

总结

建议尽可能使用初始化列表法来初始化构造函数中的成员变量,但并非所有情况都必须使用初始化列表。

一般来说,以下情况建议使用初始化列表法:

  1. 初始化成员变量:如果构造函数需要初始化类的成员变量,但此时类的结构和逻辑不是很复杂时,使用初始化列表是最清晰和高效的方式。
  2. 初始化基类部分:在派生类的构造函数中调用基类的构造函数时,应该使用初始化列表来确保基类部分被正确地初始化。
  3. 初始化const和引用类型成员变量:const和引用类型的成员变量只能在初始化列表中进行初始化,无法在构造函数体内进行赋值。
  4. 初始化具有复杂初始化逻辑的成员变量:如果某个成员变量的初始化逻辑比较复杂,使用初始化列表可以将初始化逻辑集中在一起,提高代码的可读性。

但也有一些情况可以在构造函数体内进行初始化,例如:

  1. 运行时条件决定的初始化:如果某些成员变量的初始化取决于运行时条件,可能需要在构造函数体内进行初始化。
  2. 需要在构造函数体内执行额外逻辑:如果构造函数需要执行一些额外的逻辑操作,可以在构造函数体内进行初始化。

优点和缺点

优点:

  1. (涉及含有类成员时)效率高: 使用初始化列表可以直接对成员变量进行初始化,而不需要先调用默认构造函数再进行赋值操作,从而提高了代码的执行效率。基本类型时效率与在函数体中差不多
  2. 确保成员变量的初始化: 使用初始化列表可以确保所有成员变量在对象构造时立即得到正确的初始化,避免了可能出现的未初始化的情况。
  3. 处理const和引用类型成员变量: 对于const成员变量和引用类型成员变量,只能在初始化列表中进行初始化,因为它们不能在构造函数体内被赋值。
  4. 清晰明了: 初始化列表将所有初始化操作集中在一起,使代码更加清晰易读,便于理解和维护

缺点

  1. 可读性差: 对于复杂的类结构和初始化逻辑,初始化列表可能会使代码变得复杂和难以理解,降低了可读性。
  2. 限制较多: 有些情况下,无法在初始化列表中初始化所有的成员变量,例如需要在构造函数体内进行逻辑判断后再进行初始化的情况。
  3. 容易遗忘: 在添加新成员变量时,容易忘记在初始化列表中添加相应的初始化操作,导致未初始化的错误。

使用using 重新定义访问权限

使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,可以在派生类的public成员中使用using声明指出派生类可以使用特定的基类成员,就像using指定的成员或函数是派生类的公有方法一样,即使采用的是私有派生。

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>

using namespace std;

/*一个简单的例子*/
class people
{
protected:
int age;
string name;
public:

people() = default;
people(string name,int age)
{
this->name = name;
this->age = age;
}
void showName()
{
cout << this->name << this->age << endl;
}

};

class student: private people//私有派生
{
private:
int schoolnum;
public:
using people::showName;//使用using定义重新定义访问权限
using people::name;//使用using定义重新定义访问权限

student(string name,int age,int schoolNum):people(name,age)
{
this->schoolnum = schoolNum;
}
};

int main()
{
student s("keqiudi",18,2022124018);
s.showName();//正确,使用了using可以在派生类外调用
cout << s.name << endl;//正确,使用了using可以在派生类外调用
cout << s.age << endl;//报错,未使用using,继承后为派生类private成员不可以直接访问
return 0;
}

多重继承

定义

派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。
多继承的语法也很简单,将多个基类用逗号隔开。

如已声明了类A、类B和类C,那么可以这样来声明派生类D:

1
2
3
4
5
6
7
8
9
class D: public A, private B, protected C
{
public:

protected:

private:
//类D新增加的成员
}

构造函数

与单继承形式基本相同,只是在派生类的构造函数中调用多个基类的构造函数。
以上面的 A、B、C、D 类为例,D 类构造函数的写法为:

1
2
3
4
5
D(形参列表): A(实参列表), B(实参列表), 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
57
58
59
#include <iostream>

using namespace std;

class BaseA
{
public:
BaseA()=default;
~BaseA()=default;

BaseA(int value):_value(value){};

void showValue()
{
cout << "BaseA::_value: " << _value << endl;
}
protected:
int _value;
};

class BaseB
{
public:
BaseB()=default;
~BaseB()=default;

BaseB(int value):_value(value){};

void showValue()
{
cout << "BaseB::_value: " << _value << endl;
}
protected:
int _value;
};

class Derived: public BaseA, public BaseB
{
public:
Derived()=default;
~Derived()=default;

Derived(int valueA, int valueB):BaseA(valueA), BaseB(valueB){};

void print()
{
cout << "BaseA::_value: " << BaseA::_value << endl;//使用::指明基类BaseA的value
cout << "BaseB::_value: " << BaseB::_value << endl;//使用::指明基类BaseB的value
BaseA::showValue();//使用::指明调用基类BaseA的方法
BaseB::showValue();//使用::指明调用基类BaseB的方法
}
};

int main()
{
Derived d(1, 2);
d.print();
return 0;
}

模板

类模板

在C++中,模板类是一种用于创建通用数据结构或算法的强大工具。模板类允许您编写一次代码,然后在不同数据类型上重复使用它,例如有两个或多个类,其功能是相同的,仅仅是数据类型不同时使用,以提高代码的可重用性和灵活性

模板类允许您定义一种通用的类模板,其中某些成员或函数可以根据不同进行参数化。它们使用template关键字定义

1
2
3
4
5
6
template <class T,....>
class 类模板名
{
类的定义;
};

  • typename表明其后面的符号是一种数据类型,可以用class代替
  • T是通用的数据类型,名称可以替换,通常为大写字母
  • 函数模板建议用 typename 描述通用数据类型,类模板建议用 class

示例

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

using namespace std;

template<class nameType,class ageType>//类模板
class Person
{
private:
nameType name;
ageType age;
public:
Person(nameType name,ageType age)
{
this->name = name;
this->age = age;
}
void showPerson()
{
cout << "name: " << this->name << " age: " << this->age << endl;
}
};

int main()
{
Person<string,int> student("Tom",18);
student.showPerson();
return 0;
}

函数模板

函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能,提高了程序的可重用性。

C++ 语言支持模板。有了模板,例如可以只写一个 Swap 模板,编译器会根据 Swap 模板自动生成多个 Sawp 函数,用以交换不同类型变量的值。

写法

1
2
3
4
5
6
template <typename 类型参数1,typename 类型参数2, ...>
返回值类型 模板名(形参表)
{
函数体
}
/*typename可以使用class替换*/

一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

template<typename T>//函数模板
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1, m = 2;
Swap(n, m); //编译器自动生成 void Swap (int &, int &)函数
double f = 1.2, g = 2.3;
Swap(f, g); //编译器自动生成 void Swap (double &, double &)函数
string s1 = "hello", s2 = "world";
Swap(s1, s2); //编译器自动生成 void Swap (string &, string &)函数
return 0;
}

类模板与函数模板区别

  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
//类模板与函数模板的区别
template<class NameType, class AgeType = int> //指定默认参数
class Person
{
public:
Person(NameType name, AgeType age)
{
this->m_Age = age;
this->m_Name = name;
}

void showPerson()
{
cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
}

NameType m_Name;
AgeType m_Age;
};


void test01()
{
//Person p("孙悟空", 1000);错误的,类模板无法用自动类型推导
Person<string, int>p("keqiudi", 19);//正确,只能用显式指定类型推导
p.showPerson();
}

void test02()
{
Person<string>p("keqiudi", 19); //类模板在参数列表中有默认参数
}

int main()
{
test01();

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
51
//类模板成员类外实现
template<class T1, class T2>
class Person
{
public:
Person(T1 name, T2 age);
/*内部定义{
this->m_Name = name;
this->m_Age = age;
}*/

void showPerson();
/*内部定义{
cout << "姓名:" << this->m_Name << " 年龄:" << this->m_Age << endl;
}*/

T1 m_Name;
T2 m_Age;
};

/*构造函数的类外实现,每个函数都要重新指定模板参数*/
template<class T1,class T2>//要点1:每个函数前加上template<>
Person<T1, T2>::Person(T1 name, T2 age)//要点2:类名指出模板参数再进行解析(::)
{
this->m_Name = name;
this->m_Age = age;
}


/*成员函数的类外实现,每个函数都要重新指定模板参数*/
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄:" << this->m_Age << endl;
}

void test01()
{
Person<string, int>p("Tom", 30);
p.showPerson();
}


int main()
{
test01();

system("pause");
return 0;
}

成员模板

模板可用作结构、类或模板类的成员。要完全实现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
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
template<class T>
class beta
{
private:
template<class V>//模板成员
class hold
{
private:
V val;
public:
hold(V v=0):val(v){}

void show() const{cout << val << endl;}

V Value() const {return val;}
};

hold<T> q; //模板类中的模板对象,传入的模板参数为T,即外部模板类想通过的模板参数
hold<int> n;//模板对象

public:
beta(T t,int i):q(t),n(i){}

template<typename U>//模板函数做成员
U blab(U u,T t){return (n.Value()+q.Value()*u/t);}

void Show() const{ q.show(); n.show();}
};

int main()
{
beta<double> guy(3.5,3);
cout << "T被设置为double\n";
guy.Show();
cout << "V被设置为T,即double,第二个V被设置为int\n";
cout << guy.blab(10,2.3) <<endl;
cout << "U被设置为int\n";
cout << guy.blab(10.0,2.3) <<endl;
cout << "U被设置为double\n";
cout << "Done\n";
return 0;
}

类模板对象做函数参数

一共有三种传入方式:

  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
64
65
66
67
68
//类模板对象做函数参数
template<class T1,class T2>
class Person
{
public:
Person(T1 name,T2 age)
{
this->m_Age = age;
this->m_Name = name;
}

void showPerson()
{
cout << "name: " << this->m_Name << " age:" << this->m_Age << endl;
}

T1 m_Name;
T2 m_Age;
};

//1、指定传入类型
void printPerson1(Person<string, int>&p)
{
p.showPerson();
}
void test01()
{
Person<string, int>p("孙悟空", 199);
printPerson1(p);
}


// 2、参数模板化
template<class T1,class T2>
void printPerson2(Person<T1,T2>&p)
{
p.showPerson();
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
void test02()
{
Person<string, int>p("猪八戒", 90);
printPerson2(p);
}

// 3、整个类模板化
template<class T>
void printPerson3(T &p)
{
p.showPerson();
cout << "T的类型为:" << typeid(T).name() << endl;
}
void test03()
{
Person<string, int>p("唐僧", 60);
printPerson3(p);
}

int main()
{
test01();
test02();
test03();
system("pause");
return 0;
}

将模板做模板类参数

模板可以包含类型参数(如typename T)和非类型参数(如 int n)。模板还可以本身就是模板的参数,这种参数是模板类新增的特性,用于实现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
template <template <class T> class Thing>
/*
*模板参数是template <typename T> class Thing,其中template <typename T> class 是类型,Thing是参数。
假设有Crab<King> legs;
那么King必须是一个模板类,其声明与模板参数Thing的声明匹配;
即:
template<class T>
class King{...}
*/

class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
....

}

//假设有如下声明
Crab<Stack>stack;
//成员函数Thing<int>就被替换为Stack<int>,Thing<double>替换为Stack<int>

/*
*总之模板参数Thing将被替换为声明Crab对象时被用作模板参数的模板类型
*/

类模板与继承

当类模板碰到继承时,需要注意以下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需为类模板

示例:

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
//类模板与继承
template<class T>
class Base
{
T m;
};

//class Son: public Base //错误,必须要知道父类中的T类型,才能继承给子类
class Son :public Base<int>
{

};

void test01()
{
Son s1;
}

//如果想灵活指定父类中T的类型,子类也需要变成类模板
template<class T1,class T2>
class Son2 : public Base<T2>
{
public:
Son2()
{
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;

}
T1 obj;
};

void test02()
{
Son2<int,char> s2;
}

int main()
{
test02();

system("pause");
return 0;
}

模板类和友元

模板类声明也可以有友元。模板的友元分为3类:

  1. 非模板友元

  2. 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;

  3. 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元

  • 非模板友元

    在模板类中将一个常规函数声明为友元

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
template <class T>
class HasFriend
{
public:
friend void counts();
friend void report(HasFriend<int>& hf);
friend void report(HasFriend<double>& hf);
}

/*
*上述声明使counts()函数成为模板所有实例化的友元,例如是HasFriend<int>和HasFriend<string>的友元
*report()函数也是,只不过接受一个模板类参数
*/


void counts()
{
...
}

void report(HasFriend<int>& hf)
{
...
}

void report(HasFriend<double>& hf)
{

}
  • 约束模板友元

​ 修改前一个示例,使友元函数本身成为模板,即使类的每一个具体化都获得与友元匹配的具体化,一共包含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
/*第一步,在类定义前面声明每个模板函数*/
template <typename T> void counts();
template <typename T> void report(T& t)

/*第二步,在类中将模板声明为友元,这些语句根据类模板参数的类型声明具体化*/
template <class TT>
class HasFriendT
{
...
friend void counts<TT>();
friend void report<>(HasFriendT<TT> & tt);
//或使用friend void report<HasFriendT<TT>>(HasFriendT<TT> & tt);
//声明中的<>指出这是模板具体化,<>可以为空,因为可以从函数参数推断出模板类型参数
//但counts()没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化,TT为HasFriendT类的类模板参数类型

}

/*第三步,为友元函数提供定义*/

template <typename T>
void counts()
{
...
}

template <typename T>
void report(T & hf)
{
...
}
  • 非约束模板友元

每个函数具体化都是每个类具体化的友元,友元模板类型参数与模板类类型参数不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <class T>
class ManyFriend
{
private:
T item
public:
ManyFriend(const T& i):item(i){}

template <typename C,typename D>
friend void show2(C& c,D& d);
}


template <typename C,typename D>
void show2(C& c,D& d)
{
cout << c.item << "," << d.item << endl;
}

类模板成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 对于类模板的成员函数在使用时进行实例化。当你使用特定类型实例化类模板对象时,编译器会生成该类型的成员函数实现。
  • 对于普通类,所有的成员函数都在编译时就被实例化了。
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
/*故可以做到以下的操作*/

//类模板中成员函数的创建时机
class Person1
{
public:
void showPerson1()
{
cout << "Person1 show" << endl;
}
};

class Person2
{
public:
void showPerson2()
{
cout << "Person2 show" << endl;
}
};

template<class T>
class Myclass
{
public:
T obj;

//类模板中的成员函数在调用的时候才创建,所以不会报错
void func1()
{
obj.showPerson1();
}

void func2()
{
obj.showPerson2();
}
};

void test01()
{
Myclass<Person1>m;
m.func1();
//m.func2(); 此时模板参数传递为Person1故无法调用
}

int main()
{
test01();
system("pause");
return 0;
}

类模板分文件编写(重要)

如果工程中需要利用多个类模板,那么将这些类模板都写在同一个文件中将会导致代码可读性变差,所以有必要对类模板进行分文件编写,但是类模板的分文件编写面临着一些问题,以下是类模板分文件编写面临的问题及解决方法。

问题:模板的特性导致了编译器对模板的实例化是在链接阶段进行的,而编译器需要在链接时找到模板的定义,当模板类的定义放在.h文件模板类实现放在.cpp文件,会出现链接失败情况,找不到定义。

解决方法

  • 直接包含.cpp源文件(不常用因为.cpp风格不好一般用.hpp文件)
  • 将声明和实现写到同一个文件中,并更改后缀名为.hpp

.hpp 文件通常用来包含 C++ 的头文件,其中包含类声明、函数原型、模板定义等。这种文件扩展名的选择是一种约定,用于表示这个头文件中包含一些声明与实现在一起,比如模板类

一般来说,.hpp 文件和 .h 文件在功能上是相似的,都用于包含头文件,但 .hpp 文件更常见于 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
/*编写的templatePerson.hpp文件,模板类的定义和实现放在一个文件中*/
#pragma once
#include <iostream>
using namespace std;
#include <string>

template<class T1, class T2>
class Person
{
public:
Person(T1 name, T2 age);
void showPerson();

T1 m_Name;
T2 m_Age;
};

//构造函数的类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->m_Name = name;
this->m_Age = age;
}


//成员函数的类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄:" << this->m_Age << endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*main.cpp文件内容*/
#include "templatePerson.hpp"

void test01()
{
Person<string, int> p1("Tom", 20);
p1.showPerson();
}

int main()
{
test01();

return 0;
}


模板别名(C++11)

如果能为类型指定别名,将很方便,在模板设计中尤其如此,可以使用typedef为模板具体化指定别名:

1
2
3
4
typedef std::array<double,12>arrd;
typedef std::array<int,12>arri;

......

C++11新增了一项功能——使用(using)为模板提供一系列别名,如下所示:

1
2
3
4
5
6
7
8
template<typename T>
using arrtype = std::array<T,12>;
//使用using将arrtype定义为一个模板别名,arrtype<T>表示类型std::array<T,12>。


arrtype<double> gallons;//等价于 std::array<double,12>
arrtype<int> days;//与上面相同
arrtype<std::string> months

C++11 允许将语法using = 用于非模板。用于非模板时语法与常规typedef等价:

1
2
typedef const char* pc1;
using pc2 = const char*;//与上方等价

好处

  • 使用模板别名可以提高代码的可读性,特别是在涉及复杂模板或需要引入特定模板类型时。
  • 模板别名还可以使代码更易于维护,因为你只需要在一个地方修改模板类型或表达式,而无需修改多处使用的地方。

友元

在C++中,友元类和友元函数是用来在类之间建立友好关系的机制,允许一个类的成员访问另一个类的私有成员。这样可以增加程序的灵活性和封装性。

虽然友元提供了灵活性,但过度使用友元可能会破坏封装性,因此应该谨慎使用。友元应该只在确实需要访问私有成员的情况下使用,以保持代码的清晰性和可维护性。

友元函数

  • 作用允许一个非成员函数访问类的私有成员。
  • 用法: 可以在类的声明中使用friend关键字声明友元函数,然后在类外定义这个函数。
  • 示例
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
#include <iostream>
using namespace std;

class Myclass
{
private:
int myData;
public:
explicit Myclass(int data):myData(data){}
~Myclass()=default;
Myclass()=default;

friend void showMyData(const Myclass& myclass);//友元函数声明
};

//友元函数类外定义
void showMyData(const Myclass& myclass)
{
cout << "myData = " << myclass.myData << endl;
//友元函数可以访问私有数据
}

int main()
{
Myclass myclass(10);
showMyData(myclass);
return 0;
}

友元类

  • 作用允许一个类的所有成员函数访问另一个类的私有成员
  • 用法:在类的声明中使用friend关键字声明友元类
  • 示例
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>
using namespace std;

class FriendClass
{
private:
int friendData;
public:
explicit FriendClass(int data):friendData(data){}
~FriendClass()=default;

friend class Myclass;//指定Myclass为友元类
};

class Myclass
{
private:
int myData;
public:
explicit Myclass(int data):myData(data){}
~Myclass()=default;

void showFriendData(const FriendClass& friendclass)
{
cout << "FriendClass data is: " << friendclass.friendData << endl;//友元类的成员函数直接访问另一个类的私有成员
}
};



int main()
{
FriendClass friendclass(100);
Myclass myclass(200);

myclass.showFriendData(friendclass);
return 0;
}

友元成员函数

  • 作用允许一个类的某一个成员函数访问另一个类的私有成员
  • 用法:在类的声明中使用friend关键字声明友元
  • 示例
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
#include <iostream>
using namespace std;

// 前向声明FriendClass,以便在Myclass中声明友元关系
class FriendClass;

class Myclass
{
private:
int myData;

public:
explicit Myclass(int data) : myData(data) {}
~Myclass() = default;

//声明Myclass的成员函数
void showFriendData(const FriendClass& friendclass);
};

class FriendClass
{
private:
int friendData;

public:
explicit FriendClass(int data) : friendData(data) {}
~FriendClass() = default;

// 友元函数的声明,在FriendClass中声明,使Myclass的showFriendData作为FriendClass类的友元
friend void Myclass::showFriendData(const FriendClass& friendclass);

};

// Myclass的成员函数因为是FriendClass的友元,所以定义必须在类外,用于访问FriendClass的私有成员,
void Myclass::showFriendData(const FriendClass& friendclass)
{
cout << "FriendClass data is: " << friendclass.friendData << endl;
cout << "Myclass data is: " << myData << endl;
}

int main()
{
FriendClass friendclass(100);
Myclass myclass(200);

myclass.showFriendData(friendclass);

return 0;
}

友元的优缺点

优点:

  1. 灵活性: 友元机制允许在类之间建立友好关系,使得某个函数或类可以访问其他类的私有成员,提高了灵活性。
  2. 特定需求的访问权限: 有时候,为了实现特定的功能,需要某些函数或类能够直接访问其他类的私有成员,友元机制提供了一种选择。
  3. 效率: 有时候使用友元可以避免通过公有接口的方式(比如get方法等等)进行多次函数调用,提高代码执行效率。

缺点:

  1. 破坏封装性: 友元可以直接访问类的私有成员,这可能破坏类的封装性,增加了代码的耦合性,增加了维护的难度。
  2. 复杂性增加: 过度使用友元可能导致代码更加复杂,使得程序难以理解和维护。
  3. 设计问题: 有时候需要重新考虑设计,以避免对友元的过度依赖。可能存在更好的设计模式或方法,不需要使用友元。

嵌套类

概念

在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
37
38
39
40
41
42
#include <iostream>

class Shape {
private:
// 内部类 Point 表示二维平面上的点
class Point {
private:
int x;
int y;

public:
Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {}

// 内部类的成员函数
void display() const {
std::cout << "Point: (" << x << ", " << y << ")" << std::endl;
}
};

Point center; // Shape 类包含 Point 类的对象

public:
// Shape 类的构造函数
Shape(int centerX, int centerY) : center(centerX, centerY) {}

// Shape 类的成员函数
void displayShape() const {
std::cout << "Shape with center at ";
center.display(); // 调用内部类的成员函数
}
};

int main() {
// 创建 Shape 类对象
Shape myShape(3, 4);

// 调用 Shape 类的成员函数
myShape.displayShape();

return 0;
}

作用

嵌套类的主要作用之一是组织和封装代码,使代码更为模块化、清晰和可读。以下是嵌套类的一些主要作用:

  1. 封装实现细节: 嵌套类允许将一个类的一部分实现细节封装在另一个类中,从而隐藏一些具体的实现细节,提高封装性。外部类可以更专注于公共接口,而具体的实现则留给内部类。
  2. 组织相关的类: 当两个类有密切关联时,将它们组织在一起可以形成更清晰的结构。嵌套类可以在逻辑上将这些关联的类放在同一个地方,方便代码的组织和维护。
  3. 限制访问权限: 内部类可以访问外部类的私有成员,但外部类不能直接访问内部类的私有成员。这种特性可以用于在设计中划分不同层次的访问权限,使得代码更安全。
  4. 实现接口与实现分离: 将接口和实现分离,可以通过内部类隐藏一些实现细节。外部类可以专注于公共接口,而具体的实现细节则留给内部类。
  5. 增加代码的模块化: 将相关的类组织在一起可以使代码更模块化。每个嵌套类可以有自己的职责,这样代码更易于理解和维护。

访问控制

  1. 公有嵌套类: 如果内部类声明为public,则外部类的任何成员函数、友元或其他类都可以访问内部类。这使得内部类对外可见,允许外部代码直接访问它。

  2. 私有嵌套类:如果内部类声明为private,则只有外部类的成员函数和友元可以访问内部类。外部类的客户端无法直接访问或创建内部类的对象。

  3. 保护嵌套类: 如果内部类声明为protected,则外部类的成员函数、友元以及继承了外部类的派生类都可以访问内部类

异常

异常概念

C语言异常处理机制是:

  1. 终止程序。缺陷:用户难以接受。如发生内存错误,除0错误时就会终止
  2. 返回错误码。缺陷:需要程序员自己去查对应的错误

在C++中,异常是一种在程序执行过程中发生的错误或者意外状况。异常提供了一种机制,允许程序员在代码中识别和处理错误,以及在出现错误时进行适当的响应。异常处理是C++中一个重要的编程概念,它使得程序更健壮,更容易维护。

C++中异常处理有三个关键字:throw、catch、try

  • throw:当问题出现,程序抛出一个异常。抛异常使用throw关键字完成。
  • catch:用于捕捉异常。**catch(…)**可以捕获任意类型的异常,主要时用来捕获没有显示捕获类型的异常。相当于条件判断中的else。
  • try:try中包含会出现异常的代码或者函数。后面通常会跟一个或者多个catch块。

注意:可以抛出任意类型的对象。抛出的异常必须捕获。try要和catch匹配使用,catch里的内容抛出异常时才执行,没有异常,不执行。

示例

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
#include <iostream>
using namespace std;

int test(){
int a, b;
cout << "请输入被除数和除数:" << endl;
cin >> a >> b;
if (b == 0){
throw "除0错误";//抛出异常,这里抛出的是字符串类型异常,也可以是对象
}
return a / b;
}

int main(){

try{
cout << test() << endl;//会出现异常的代码
}

//捕获字符串类型的异常,a即 "除0错误"。
catch (const char* a){
cout << a << endl;
}
//捕获任意类型的异常,通常未知异常
catch (...){
cout << "unknow exception" << endl;
}

return 0;
}

异常的抛出和匹配规则

异常的抛出

  1. 异常时通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。

  2. 被选中的处理代码的调用链是,找到于该类型匹配且离抛出异常位置最近的那一个catch。

  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。

  4. catch(…)可以捕获任意类型的对象,主要是用来捕获没有显示捕获类型的异常,因为如果没有匹配的catch会终止程序。相当于条件判断中的else。问题是不知道异常错误是什么。

  5. 实际中抛出和捕获的类型不一定类型完全匹配,可以抛出派生类对象,使用基类来捕获,这个在实际生活中很实用。主要原因是:派生类可以赋值给基类。

匹配规则

  • 首先检查throw本身是否在try块内部,如果是,再在当前函数栈中查找匹配的catch语句。如果有匹配的直接跳到catch的地方执行。
  • 如果没有匹配的catch块,则退出当前函数栈,在调用函数的栈中查找匹配的catch。
  • 如果到达main函数的栈,都没有匹配的catch,就会终止程序。
  • 上述沿着调用链查找匹配的catch块的过程叫栈展开。所以实际要最后要加一个catch(…)来捕获任意类型的异常,防止程序终止。
  • 找到匹配的catch会直接跳到catch语句执行,执行完后,会继续沿着catch语句后面执行。

异常的重新抛出

有可能单个的catch不能完全处理一个异常,**在进行一些矫正处理后,需要交给更外层的调用链函数来处理。**catch可以做完矫正操作,再将异常重新抛出,交给更上层的函数进行处理。

异常安全问题

由于抛异常只要找到匹配的catch就直接跳到catch块执行,没有找到对应catch的函数就不会继续执行。这样导致函数的执行流回很乱。可能会导致一些问题。

  • 构造函数完成对象的构造和初始化,最好不要再构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化

  • 析构函数主要完成资源的清理,最好不要在析构函数中抛异常,否则可能导致内存泄漏。

  • C++异常经常会导致资源泄漏问题。比如:在new和delete中抛出异常,导致new出来的资源没有释放,导致内存泄漏。在lock和unlock中抛出异常,导致锁没有释放,导致死锁。

有两种解决办法:

  • 将异常捕获,释放资源后,将锁重新抛出。
  • 使用RAII的思想解决。定义一个类封装,管理资源。当要使用时实例化一个类对象,将资源传入,当退出函数,调用对象析构函数,释放资源。

异常规范说明

  • noexcept:表示函数不抛出异常。
1
2
3
void myFunction() noexcept {
// 函数体
}
  • noexcept(expression): 表示如果 expression 求值结果为 true,则函数不抛出异常。
1
2
3
void myFunction() noexcept(someExpression()) {
// 函数体
}

早期的异常规范: 在早期的C++标准中,可以使用 throw() 表示函数不抛出异常,或者使用 throw(type1, type2, ...) 来指定函数可能抛出的异常类型。

1
2
3
void myFunction() throw(); // 函数不抛出异常
void anotherFunction() throw(std::exception, MyException); // 函数可能抛出 std::exception 或 MyException

现代C++中的 noexcept: 推荐使用 noexcept 关键字来表示函数是否抛出异常。

1
2
3
void myFunction() noexcept; // 函数不抛出异常
void anotherFunction() noexcept(someExpression()); // 函数在 someExpression() 为 true 时不抛出异常

异常规范的问题: 早期的异常规范在实践中并没有提供太多的好处,而且容易导致问题。因此,自C++11开始,异常规范已经被弃用,而 noexcept 关键字更灵活且更安全。

自定义异常体系

在实际中,并不是我们想抛什么异常就抛什么异常,这样会导致捕捉的时候不好捕捉。而是,会建立一个继承体系,建立一个异常类,派生类继承这个类,来定义出不同的异常。

  • 到时候抛出异常**,只需要用基类进行捕捉即可**
  • 基类可以相当于是一个框架,派生类是具体的异常。然后去具体实现异常的内容,然后抛异常只需要抛派生类,捕捉异常只需要捕捉基类即可。

下列使用一个简单实现:

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
#include <iostream>
using namespace std;

//基类
//异常
class Exception{
public:
Exception(const char* str = nullptr, int id = 0):_errmsg(str), _id(id){}

virtual void what()const = 0;//派生类中输出的指定异常信息
protected:
string _errmsg;//错误信息
int _id;//错误码
};
//派生类
//数据库异常
class SqlException :public Exception{
public:
SqlException(const char *str = nullptr, int id = 1)
:Exception(str, id)
{}

virtual void what()const{
cout << "error msg:" << _errmsg << endl;
cout << "error id:" << _id << endl;
}
};
//网络异常
class HttpException :public Exception{
public:
HttpException(const char *str = nullptr, int id = 2)
:Exception(str, id)
{}

virtual void what()const{
cout << "error msg:" << _errmsg << endl;
cout << "error id:" << _id << endl;
}
};
//缓存异常
class CacheException :public Exception{
public:
CacheException(const char *str = nullptr, int id = 3)
:Exception(str, id)
{}

virtual void what() const{
cout << "error msg:" << _errmsg << endl;
cout<< "error id:" << _id << endl;
}
};

void test(){
//当网络连接失败,抛出这个异常即可
//throw HttpException("Http fail", 2);
//当缓存错误,抛出这个异常
//throw CacheException("Cache error", 3);
//当数据库错误
throw SqlException("Sql error", 4);//抛出派生类异常对象
}

int main(){
try{
test();
}
//捕获基类异常,
catch (const Exception& a){
a.what();//输出错误信息
}
catch (...){
cout << "unknow exception" << endl;
}
system("pause");
return 0;
}

C++标准库的异常体系

在C++库中也建立了一个异常体系。也给我们提供了一些异常类。我们可以在程序中使用这些标准异常,它们也是以父子类层次结构组织起来的。

  • 基类:std::exception
  • 派生类:std::runtime_error等

异常优缺点

优点

  • 异常对象定义好了,相比较于错误码,可以清晰准确的展示出错误的各种信息,甚至包含堆栈调用信息,可以帮我们很好的定位程序的bug。
  • 在函数调用链中,深层函数返回错误,我们得层层返回,需要不断的判断是什么错误,再返回给最外层。异常直接会找到对应的catch执行,不需要判断是什么错误。
  • 部分函数更好处理,比如没有返回值的函数或者返回值为自身的T& operator,不好返回错误码。并且pos越界了,内存错误等不需要终止程序。
  • 更好的进行测试代码

缺点

  • 异常导致执行流乱跳,运行混乱。导致我们调试和分析程序时,比较困难。
  • C++没有垃圾回收机制,可能会导致异常安全问题。开辟的资源和打开的流,由于执行流乱跳,导致没有释放和关闭等。导致内存泄漏。打开的锁为关闭,导致死锁。
  • C++标准库的异常体系定义不好,导致我们需要各自定义各自的体系,非常混乱。
  • 随意抛异常,外层不好捕获,所以尽量按找异常规范使用。

RTTI

在C++中,RTTI是运行阶段类型识别的简称(Runtime Type Identification)。它允许在程序运行时获取对象的类型信息。是新添加到C++中的特性之一,很多老式实现不支持。主要用途包括:

  1. 识别对象类型: RTTI允许在程序运行时确定对象的类型。这对于处理多态性和动态多态性(dynamic polymorphism)非常有用。例如,在一个基类的指针或引用指向派生类对象时,你可以使用RTTI来确定实际派生类的类型。
  2. 安全的类型转换: 在某些情况下,你可能需要将基类指针或引用安全地转换为派生类指针或引用。RTTI的dynamic_cast运算符可用于执行这种安全的转换,它会在转换之前检查类型信息,如果转换不安全,它会返回空指针或引发异常,而不是导致未定义行为。
  3. 类型检查: RTTI允许你在运行时检查对象的类型信息,从而采取相应的措施。这对于实现一些通用的算法或框架,需要根据对象的类型来执行不同的操作,非常有用。
  4. 异常处理: 在一些情况下,RTTI可用于处理异常。当在基类指针或引用上使用dynamic_cast时,如果转换失败,会返回空指针或引发std::bad_cast异常,可以在异常处理中捕获。

C++中有三个支持RTTI的元素

  • dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回空指针-0
  • typeid运算符返回一个指出对象的类型的值
  • type_info结构储存了有关特定类型的信息

dynamic_cast

dynamic_cast 操作符dynamic_cast 用于在运行时执行安全的类型转换,主要用于处理继承关系。它可以将指向基类的指针或引用转换为派生类的指针或引用,同时执行类型检查。

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

class Base {
virtual void dummy() {}
};

class Derived : public Base {
};

int main() {
Base* basePtr = new Derived();

Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

if (derivedPtr != nullptr)
{
std::cout << "Successfully casted to Derived." << std::endl;
}
else {
std::cout << "Failed to cast to Derived." << std::endl;
}

delete basePtr;
return 0;
}

1
2
3
4
5
6
7
try {
Derived& derivedRef = dynamic_cast<Derived&>(*basePtr);
// 成功转换
} catch (const std::bad_cast& e) {
// 转换失败
}
/*在一些情况下,RTTI可用于处理异常。当在基类指针或引用上使用dynamic_cast时,如果转换失败,会返回空指针或引发std::bad_cast异常,可以在异常处理中捕获。*/

typeid和type_info

typeid运算符

  • 用途: typeid运算符用于在运行时获取对象的类型信息。它返回一个const std::type_info& 对象,该对象包含有关实际类型的信息,其中type_info是在头文件typeinfo(以前是typeInfo.h)中定义的一个类。。
  • 语法: typeid(expression),其中expression是一个表达式,通常是一个对象或一个类型

type_info类

  • 用途: type_info是一个类,表示类型信息。它包含有关类型的信息,例如类型的名称。
  • 成员函数:
    • name() 返回一个指向包含类型名称的C字符串的指针。请注意,这个名称的格式是实现定义的,可能在不同编译器和平台上有所不同。
    • 其他成员函数: 可能会有其他一些实现特定的成员函数,但标准并没有规定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <typeinfo>

class Base {
// ...
};

class Derived : public Base {
// ...
};

int main() {
Base* basePtr = new Derived();
const std::type_info& typeInfo = typeid(*basePtr);
std::cout << "Object type: " << typeInfo.name() << std::endl;

delete basePtr;
return 0;
}

typeid运算符使得能够确定两个对象是否为同种类型。type_info类重载了==和!运算符,可以使用这些运算符来对类型比较。

例如

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

class Base {
virtual void dummy() {}
};

class Derived : public Base {
};

int main() {
Base* basePtr = new Base;
Derived* derivedPtr = new Derived;

std::cout << (typeid(Base) == typeid(*basePtr)) << std::endl;//(*basePtr表示指向的对象)相同,返回值1
std::cout << (typeid(Base) == typeid(*derivedPtr)) << std::endl;//返回值0
std::cout << (typeid(Derived) == typeid(*basePtr)) << std::endl;//返回值0
std::cout << (typeid(Derived) == typeid(*derivedPtr)) << std::endl;//返回值1

std::cout << (typeid(*basePtr) == typeid(*derivedPtr)) << std::endl;//,对象不同返回值0
delete basePtr;
delete derivedPtr;
return 0;
}

智能指针模板类

当谈到C++中的智能指针时,通常会涉及到 std::shared_ptrstd::unique_ptr 这两个模板类。还有一个std::auto_ptr已经被摒弃,但在C++11以前使用了多年,如果编译器不支持其他两种,则auto_ptr是唯一选择。这些智能指针类旨在管理动态分配的内存,并在对象不再需要时自动释放该内存,从而避免内存泄漏和悬挂指针的问题。

要创建智能指针对象,必须包含头文件memory,该文件模板定义。然后使用通常的模板语法来实例化所需类型的指针。

auto_ptr模板类

std::auto_ptr 是 C++98 标准中引入的智能指针,用于管理动态分配的内存。然而,它在 C++11 标准中已被弃用,并且在 C++17 标准中已经被移除。主要原因是 std::auto_ptr 存在一些问题,特别是在资源所有权转移方面存在潜在的危险。

一个简单的例子:

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

int main() {
std::auto_ptr<int> autoPtr1(new int(42));
*autoPtr1 = 1;
// autoPtr2 现在拥有 autoPtr1 的内存所有权
std::auto_ptr<int> autoPtr2 = autoPtr1;

std::cout << *autoPtr1 << std::endl;//输出1
// 这里autoPtr1不再拥有资源,会导致运行时错误
// 使用 autoPtr1 时可能会出现未定义的行为
// ...
std::auto_ptr<double> autoPtr3(new double);
*autoPtr3 = 3.14;
std::cout << *autoPtr3 << std::endl;//输出3.14

std::auto_ptr<string> autoPtr4(new string);
return 0;
}

std::auto_ptr具有独占所有权的特性,但其所有权转移的方式可能导致一些问题,因为它采用了移动语义而非拷贝语义。这意味着当一个 std::auto_ptr所有权转移给另一个时,原始的 std::auto_ptr 将不再拥有对资源的所有权。

shared_ptr模板类

std::shared_ptr 是一种共享所有权的智能指针,多个 shared_ptr 实例可以共享对同一块内存的所有权。它使用引用计数来追踪有多少个 shared_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
#include <memory>

int main() {

// 创建一个shared_ptr并分配内存
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
/*
* 等价于std::shared_ptr<int> sharedPtr(new int(42));
* 但是std::make_shared<int>(42)更加安全,因为它有性能优势和异常安全性
*
*/

// 共享所有权
std::shared_ptr<int> anotherSharedPtr = sharedPtr;

std::cout << "sharedPtr: " << *sharedPtr << std::endl;
std::cout << "anotherSharedPtr: " << *anotherSharedPtr << std::endl;
// 使用sharedPtr和anotherSharedPtr
// ...

// 当最后一个shared_ptr离开作用域时,内存会被自动释放
return 0;
}

unique_ptr模板类

std::unique_ptr 是一种独占所有权的智能指针,一个 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
27
28
29
30
#include <memory>

int main() {
// 创建一个unique_ptr并分配内存
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42);
std::cout << *uniquePtr << std::endl;//输出42
/*
* 等价于std::unique_ptr<int> sharedPtr(new int(42));
* 但是std::make_unique<int>(42)更加安全,因为它有性能优势和异常安全性
*
*/




// unique_ptr不能直接赋值给另一个unique_ptr(会发生所有权转移)
// std::unique_ptr<int> anotherUniquePtr = uniquePtr; // 错误,编译失败

// 可以通过std::move进行所有权转移
std::unique_ptr<int> anotherUniquePtr = std::move(uniquePtr);

std::cout<< *anotherUniquePtr << std::endl;//输出42

// 使用anotherUniquePtr
// ...

// 当anotherUniquePtr离开作用域时,内存会被自动释放
return 0;
}

unique_ptr为何优于auto_ptr

  1. 更安全的所有权转移: std::unique_ptr 使用移动语义进行所有权转移,而 std::auto_ptr 使用复制语义。由于 std::auto_ptr 的复制语义可能导致不明确的行为,因此在 C++11 引入 std::unique_ptr 时,std::auto_ptr 被标记为已弃用。使用 std::unique_ptr 更容易理解和更安全。

  2. 支持数组和自定义删除器: std::unique_ptr 可以用于管理数组(std::unique_ptr<T[]>),而 std::auto_ptr 不支持这种用法。此外,std::unique_ptr 还支持通过自定义删除器来管理非默认方式分配的资源。

    1
    2
    3
    4
    5
    6
    std::unique_ptr<int[]> arrayPtr(new int[5]);

    arrayPtr[0] = 1;
    arrayPtr[1] = 2;
    arrayPtr[2] = 3;
    ...
  3. 更灵活的模板参数: std::unique_ptr 具有更灵活的模板参数,可以轻松地与自定义删除器和分配器一起使用。这提供了更多的灵活性,以适应各种资源管理需求。

  4. 更严格的所有权管理: std::unique_ptr 严格实现了独占所有权的概念,一个 std::unique_ptr 实例独立拥有对其指向的资源的所有权。这使得代码更加明确,减少了潜在的错误。

警告

只有使用new分配内存时,才能使用auto_ptr和shared_ptr,使用new[] 分配内存时,不能使用它们,只能使用unique_ptr。不使用new或new[]分配内存时,也不能使用unique_ptr。

标准模板库STL

模板类vector

vector又名动态数组

C++ vector容器详解_c++容器vector-CSDN博客

基于范围的for循环

基于范围的for循环是为用于STL而设计的。在这种for循环中,括号中的代码声明一个类型(通常用auto)与容器储存的内容相同的变量,然后指出了容器的名称。接下来,循环体使用指定的变量依次访问容器的每个元素。若使用引用参数,则可修改容器内容

一个简单的例子:

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

int main() {

int price[5] = {1,2,3,4,5};
std::vector<int> price2 = {1,2,3,4,5};

/*未使用引用参数,不能修改容器内容*/
for(auto x: price) {
std::cout << x << std::endl;
}
/*使用引用可以修改容器内容*/
for(auto &x: price2) {
x=10;
std::cout << x << std::endl;
}
return 0;
}

泛型编程

STL是一种泛型编程。面向对象编程关注的是编程的数据方面,而泛型编程关注的则是算法。他们之间的共同点是抽象和创建可重用代码,但他们的理念绝然不同。

泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。模板使得能够按泛型定义函数或类,而STL通过通用算法更近了一步。为了解模板和设计是如何协同工作的,我们需要先了解一下迭代器。

迭代器

在C++中,模板使得算法独立于储存的数据类型,而迭代器使算法独立于使用的容器类型,迭代器(Iterator)是一种用于遍历容器(如数组、向量、链表等)中元素的对象。迭代器提供了一种统一的方式来访问容器中的元素,而不必关心容器的具体类型或实现细节。C++标准库提供了多种类型的迭代器,主要分为五种

  1. Input Iterator(输入迭代器)
    • 只允许从容器中读取元素,但不能修改元素。
    • 支持逐个递增,只能用于单向遍历。
  2. Output Iterator(输出迭代器)
    • 只允许往容器中写入元素,但不能读取元素。
    • 支持逐个递增,也只能用于单向遍历。
  3. Forward Iterator(前向迭代器)
    • 具有Input Iterator和Output Iterator的功能,支持读写操作。
    • 支持逐个递增,可用于单向遍历。
  4. Bidirectional Iterator(双向迭代器)
    • 具有Forward Iterator的功能,同时支持逐个递减。
    • 支持双向遍历,即可以前进也可以后退。
  5. Random Access Iterator(随机访问迭代器)
    • 具有Bidirectional Iterator的功能,同时支持随机访问元素。
    • 支持通过指针算术运算(如 +、-)直接跳跃访问容器中的元素。

在C++标准库中,不同的容器提供不同类型的迭代器,例如,std::vectorstd::list提供了双向迭代器,而std::arraystd::deque提供了随机访问迭代器。在使用迭代器时,要注意选择适当的类型以满足操作的需求。。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*定义一个迭代器...*/
vector<type>::iterator it;
list<type>::iterator it2;



/*一个简单的示例*/
vector<int> price= {1,2,3,4,5};
list<double>::iterator it2;//定义的一个遍历list容器的迭代器
vector<int>::iterator it = price2.begin();//定义的一个遍历vector容器的迭代器
for(; it != price2.end(); it++)//迭代器支持前缀++和后缀++,遍历容器
{
std::cout << *it << std::endl;
}

实际上作为一种编程风格,最好避免直接使用迭代器,而应尽可能使用STL函数(for_each())来处理细节。也可以使用C++11新增的基于范围的for循环。

容器

STL具有容器概念和容器类型。概念是具有名称(容器、序列容器、关联容器)的通用类别,容器类型是可用于创建具体容器对象的模板。

主要的容器分类包括:

  1. 序列容器(Sequence Containers)
    • 顺序存储元素,元素的顺序与它们被插入的顺序相同。
    • 包括:
      • std::vector: 动态数组,支持快速随机访问。
      • std::list: 双向链表,支持在任意位置快速插入和删除元素。
      • std::deque: 双端队列,支持在两端快速插入和删除元素。
      • std::array: 固定大小的数组,支持快速随机访问。
  2. 关联容器(Associative Containers)
    • 基于键值对(Key-Value)的存储方式,通过键值来快速查找元素。
    • 包括:
      • std::set: 有序集合,不允许重复元素。
      • std::map: 有序映射,存储键值对,不允许重复的键。
      • std::multiset: 有序集合,允许重复元素。
      • std::multimap: 有序映射,允许重复的键。
  3. 无序容器(Unordered Containers)
    • 使用哈希表实现,元素的存储顺序不固定。
    • 包括:
      • std::unordered_set: 无序集合,不允许重复元素。
      • std::unordered_map: 无序映射,存储键值对,不允许重复的键。
      • std::unordered_multiset: 无序集合,允许重复元素。
      • std::unordered_multimap: 无序映射,允许重复的键。
  4. 容器适配器(Container Adapters)
    • 提供特定接口的封装,简化了底层容器的使用。
    • 包括:
      • std::stack: 栈,后进先出(LIFO)。
      • std::queue: 队列,先进先出(FIFO)。
      • std::priority_queue: 优先队列,按照优先级排序。

用法见C++常用容器-CSDN博客

函数对象

在C++中,函数对象(Function Objects),也称为函数符或仿函数(Functor),函数符是可以以函数方式与()结合使用的任意对象。这包括函数名的指针和重载了()运算符对象的类(即定义了函数operator()()的类)。是一种可调用对象,可以像函数一样被调用。函数对象通常是类对象,但不像普通函数,它们可以携带状态信息,并可以通过成员函数实现自定义行为。函数对象可用于算法、STL容器等各种场景中。

函数对象类

1
2
3
4
5
6
7
8
9
// 函数符类
class AddFunctor {
public:
// 重载函数调用运算符
int operator()(int a, int b) {
return a + b;
}
};

使用方式

1
2
AddFunctor addFunctor;
int result = addFunctor(3, 4); // 结果为7

应用

  1. 函数对象作为算法的参数

函数对象可以作为算法的参数,提供一种灵活的方式来定制算法的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <algorithm>
#include <vector>

struct SquareFunctor {
int operator()(int x) const {
return x * x;
}
};

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
SquareFunctor squareFunctor;

// 使用函数对象对容器中的每个元素进行平方操作
std::transform(numbers.begin(), numbers.end(), numbers.begin(), squareFunctor);

// 现在numbers为 {1, 4, 9, 16, 25}
return 0;
}

  1. 函数对象与STL

函数对象在STL(标准模板库)中广泛应用,例如在排序、查找等算法中可以通过函数对象来指定比较的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <algorithm>
#include <iostream>
#include <vector>

struct DescendingOrder {
bool operator()(int a, int b) const {
return a > b;
}
};

int main() {
std::vector<int> numbers = {5, 2, 8, 1, 3};
DescendingOrder descendingOrder;

// 使用函数对象进行降序排序
std::sort(numbers.begin(), numbers.end(), descendingOrder);

// 现在numbers为 {8, 5, 3, 2, 1}
return 0;
}

算法

STL的算法

  • 作用范围:STL的算法是独立于容器的通用算法。它们被设计为能够在不同类型的容器上进行操作,而不依赖于具体容器的实现。这种独立性使得同一个算法可以用于不同的数据结构,例如,可以使用std::sort在不同类型的容器上进行排序。
  • 参数类型:STL的算法通常接受迭代器(iterator)作为参数,因此可以用于各种容器,如数组、向量、链表等。它们不直接与容器关联,而是通过迭代器与容器交互。
  • 功能丰富:STL的算法涵盖了广泛的应用场景,包括排序、查找、数学运算、变换等。这些算法是为了提供通用且高效的数据处理工具。

以下是一些常用的STL算法及其简要介绍:

  1. 排序算法
    • std::sort:对容器进行排序,默认是升序排序。可以传递自定义的比较函数或Lambda表达式来实现不同的排序规则。
    • std::stable_sort:稳定排序,保持相等元素的相对顺序。
    • std::partial_sort:部分排序,将容器中的一部分元素排序,其余元素不变。
  2. 查找算法
    • std::find:在容器中查找指定值的第一个出现位置。
    • std::binary_search:在已排序的容器中进行二分查找。
    • std::count:统计容器中指定值的出现次数。
  3. 变换算法
    • std::transform:将一个范围的元素转换为另一个范围,可以结合函数对象或Lambda表达式进行元素的变换操作。
    • std::copy:将一个范围的元素复制到另一个范围。
    • std::replace:替换容器中指定值的所有出现。
  4. 删除和修改算法
    • std::remove:在容器中移除指定值的所有元素,不改变容器大小,返回一个新的结束迭代器。
    • std::remove_if:根据谓词条件移除满足条件的元素。
    • std::unique:移除容器中相邻的重复元素,仅保留一个。
  5. 数值算法
    • std::accumulate:对范围内的元素进行累积操作,可以用于计算总和、平均值等。
    • std::inner_product:计算两个范围的内积。
    • std::iota:用给定的值填充一个范围。
  6. 其他算法
    • std::minstd::max:找到范围内的最小值和最大值。
    • std::reverse:将容器中的元素进行反转。
    • std::rotate:将容器中的元素进行旋转。

C++标准库中还有很多其他有用的算法。使用STL算法,可以编写更简洁、可读性更好的代码,并且由于这些算法经过优化,通常具有较好的性能

STL总结

STL(Standard Template Library)是C++标准库的一部分,提供了一套通用的模板类和函数,用于处理常见的数据结构和算法。STL的设计目标是提供高效、灵活、可复用的代码,以便开发者能够更加专注于解决问题而不必重复实现基础数据结构和算法。STL主要包括以下三个组件

  1. 容器:序列容器、关联容器等等
  2. 算法:提供了一系列通用的算法,如排序、查找、变换、合并等。这些算法可以用于不同类型的容器,通过迭代器进行操作,实现了数据结构和算法的分离,增强了代码的可复用性和通用性。算法通过函数对象或函数指针支持用户自定义的操作和比较规则。
  3. 迭代器:迭代器是STL中用于遍历容器元素的通用接口,为算法和容器提供了统一的访问机制。不同类型的容器支持不同类型的迭代器,包括输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器,提供了不同程度的功能和效率。

其他库

vector、valarray、array

C++标准库提供了多个数组模板(std::vectorstd::valarraystd::array),每个模板都有其特定的用途和优势。这样设计的目的是为了满足不同的编程需求,提供更丰富、更灵活的选择。以下是这三个数组模板的主要区别和适用场景:

  1. std::vector
  • 动态大小std::vector是一个动态数组,其大小可以在运行时动态调整。这使得它非常适用于需要动态增减大小的场景,例如在运行时读取不确定数量的数据。
  • 内存管理std::vector会自动处理内存的分配和释放,使得在动态数组的使用上更加方便。
  1. std::valarray
  • 数值运算std::valarray设计用于面向数值计算,提供了一些成员函数和操作符用于逐元素进行数学运算。它的目标是提高数值计算的效率。
  • 元素级别的操作std::valarray更适用于执行逐元素的数学运算,如数组的逐元素加法、乘法等。
  1. std::array
  • 固定大小std::array是一个静态数组,其大小在编译时就确定了,不能动态改变。这使得它适用于固定大小的场景,例如需要在编译时确定数组大小的情况。
  • 栈上分配std::array通常在栈上分配内存,因此相较于动态数组,它的内存访问更加高效。

模板initializer_list

在C++中,initializer_list 是一个标准库中的类模板,用于方便地初始化容器或其他类的对象。它允许在对象的构造函数中传递一个初始化列表,类似于数组的初始化方式。initializer_list 的定义位于头文件 <initializer_list> 中。

  • 用于容器的构造函数:使得容器类如 std::vectorstd::initializer_list 等能够通过初始化列表进行构造。
1
2
3
4
5
6
7
8
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// ...
return 0;
}
//vector类构造函数中参数使用了initializer_list
  • 容器和类的构造函数重载:类可以同时提供接受 initializer_list 和其他参数的构造函数,以便支持不同的初始化方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
public:
MyClass(int a, double b) {
// ...
}

MyClass(std::initializer_list<int> values) {
// ...
}
};

int main() {
MyClass obj1(10, 3.14);
MyClass obj2 = {1, 2, 3, 4};//使传递一个初始化列表
// ...
return 0;
}

输入、输出和文件

流和缓冲区

在C++中,流(stream)和缓冲区(buffer)是与输入和输出相关的概念,用于处理数据的流动和存储。流提供了一个抽象层,使得输入和输出可以以统一的方式进行处理,而缓冲区则用于临时存储数据,以提高性能和效率。

流(Stream)

流是一个抽象的概念,表示数据在程序和外部设备(如文件、屏幕、键盘等)之间的传输。在C++中,标准库提供了一些流类(如iostream、fstream、stringstream等),用于实现输入和输出的操作。流可以分为输入流和输出流,分别用于读取和写入数据。

常见的流类包括:

  1. iostream 提供了cin(标准输入流)和cout(标准输出流),用于从键盘读取输入和向屏幕输出数据。

  2. fstream 用于文件输入和输出,包括ifstream(文件输入流)和ofstream(文件输出流)。

  3. stringstream 用于在内存中操作字符串,可以将字符串作为输入或输出流处理。

缓冲区(Buffer)

缓冲区是用于暂时存储数据的区域,它可以提高输入输出的效率。标准库中的流都具有与之关联的缓冲区,用于暂时存储数据,然后一次性地进行读取或写入,而不是每次都直接与外部设备进行通信。

流的缓冲区可以分为两种:

  1. 输入缓冲区: 存储从外部设备(如键盘或文件)读取的数据,以便程序可以逐一处理。
  2. 输出缓冲区: 存储要写入外部设备的数据,以提高写入效率。

在一些情况下,你可能需要手动刷新缓冲区,以确保数据被及时处理。使用flush()函数可以强制将输出缓冲区的内容写入外部设备。

iostream文件

iostream是C++标准库中的头文件之一,它包含了对输入和输出流的支持。具体而言,iostream是由两个基本的头文件合并而成,分别是:

  • istream(Input Stream): 用于输入流,提供了从输入设备(如键盘)读取数据的功能。主要的类包括istreamifstream
  • ostream(Output Stream): 用于输出流,提供了向输出设备(如屏幕或文件)写入数据的功能。主要的类包括ostreamofstream

通过合并这两个头文件,得到了iostream,其中包括了cincoutcerrclog等标准流对象,以及相关的功能和操作符重载,使得输入输出操作变得简便和灵活。

C++的iostream库管理了很多细节。例如在程序中包换iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)

常见的8个流对象及其用途包括:

  • cin 标准输入流,用于从用户输入中读取数据,wcin对象于此类似但处理的是wchar_t(宽字符类型)类型。
  • cout 标准输出流,用于将数据输出到控制台。wcout对象于此类似但处理的是wchar_t(宽字符类型)类型。
  • cerr 标准错误流,没有被缓冲,用于输出错误信息到控制台。wcerr对象于此类似但处理的是wchar_t(宽字符类型)类型。
  • clog 标准日志流,用于输出程序运行时的一般信息。wclog对象于此类似但处理的是wchar_t(宽字符类型)类型。

cerr和clog

在C++中,cerrclog都是标准错误流,用于输出错误信息到控制台。它们是ostream类的实例,提供了与cout相似的输出功能,但通常用于不同的目的。

  1. cerr(标准错误流):

    • cerr是一个标准错误流对象,用于输出程序的错误信息。
    • cout不同,cerr输出默认不被缓冲,意味着错误消息会立即显示在控制台上,而不受缓冲机制的影响
    • 通常用于输出紧急的错误信息,以便及时发现问题。

    示例:

    1
    2
    3
    4
    5
    #include <iostream>
    int main() {
    std::cerr << "This is an error message." << std::endl;
    return 0;
    }
  2. clog(标准日志流):

    • clog也是一个标准错误流对象,用于输出程序运行时的一般信息,类似于日志。
    • cerr不同,clog的输出默认是被缓冲的,可以通过std::flush强制刷新输出,或者等到缓冲区满时才刷新。
    • 通常用于输出程序的运行时信息,方便调试和了解程序执行的进展

    示例:

    1
    2
    3
    4
    5
    6
    #include <iostream>

    int main() {
    std::clog << "This is a log message." << std::endl;
    return 0;
    }

总体而言,cerrclog都是用于输出程序的诊断信息的标准错误流,它们与cout一起构成了C++中的标准流。选择使用哪一个取决于你的需求,如果你需要及时看到错误信息,可以使用cerr,如果你希望输出日志信息,并允许一定程度的缓冲,可以使用clog

使用cout进行输出

重载的<<运算符

在C++中,<<运算符的默认含义是按位左移运算符,但ostream类重新定义了<<运算符,将其重载为输出。在这种情况下<<能识别C++中的所有的基本类型(int、double、string…)

对于上述每种类型,ostream类提供了operator<<()函数的定义。

<<运算符的所有化身返回类型都是ostream&,格式如下:

1
ostream& operator<<(type);

意味着该运算符将返回一个指向ostream对象的引用,该引用指向用于调用该运算符的对象。例如:

1
2
cout << "hello world";
// <<运算符返回的是cout对象

这种特性使得cout能串联输出:

1
cout << "hello world" << value << "oh";

其他osteam方法

ostream还提供了put()方法和write()方法

put:用于显示字符

write:用于显示字符串

  • put原型:将一个字符插入到输出流中。
1
ostream& put(char)

示例:

1
2
3
4
cout.put('W');
cout.put('I').put('t');//可进行拼接输出
cout.put(65) //输出A
cout.put(66.3) //自动将double值66.3转换为char值66,输出B
  • write模板原型: 将指定数量的字符从指定位置的字符串插入到输出流中。

    1
    2
    3
    4
    basic_ostream<char, Traits>& write(const char* s, streamsize n);

    -第一个参数为指定字符串
    -第二个参数为指定长度

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>
    using namespace std;

    int main() {
    const char* message = "Hello, C++!";
    cout.write(message, 6); // 将 "Hello" 写入输出流

    return 0;
    }

刷新输出缓冲区

  • 在C++中,刷新输出缓冲区是指将缓冲区中的数据强制写入输出设备。默认情况下,输出流(如coutcerr等)会将数据存储在内部缓冲区中,而不是每次写入一个字符就立即刷新到输出设备。刷新输出缓冲区的操作可以通过 flush 方法或 flush 操纵符来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    /*第一种*/
    cout << "This is some text." << flush; // 刷新输出缓冲区

    /*第二种*/
    cout << "This is some text." << endl; // endl 操纵符会输出一个换行符并刷新输出缓冲区

    /*第三种*/
    flush(cout);

用cout进行格式化

  1. 控制输出的进制:在下一次将格式状态修改为其他进制时才会输出其他进制,否则一直按照设置进制之后进行输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main() {
int num = 255;

// 十六进制输出
cout << hex << "Hexadecimal: " << num << endl;
/*或者使用hex(cout)、oct(cout)*/
// 八进制输出
cout << oct << "Octal: " << num << endl;

return 0;
}
//hex,oct都是控制符
  1. 调整字段宽度和对齐方式:width()方法只影响接下来显示的一个项目,然后字段宽度将恢复为默认值

使用width()进行设置字段宽度,left左对齐,right右对齐

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

int main() {
int num1 = 123;
double num2 = 45.6789;

// 控制输出宽度和对齐方式
cout.width(10);
cout << left << num1;

cout.width(10);
cout << right << num2 << endl;

return 0;
}
  1. 填充字符

在默认情况下,cout用空格填充字段中未被使用的部分,可以使用fill()成员函数来改变填充字符。在下次修改之前将一直使用之前修改的字符填充

1
cout.fill('*');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
int num1 = 123;
double num2 = 45.6789;
cout.fill('*');
// 控制输出宽度和对齐方式
cout.width(10);//宽度为10,前7位用*填充
cout << right << num1;
cout.width(10);
cout << right << num2 << endl;

return 0;
}
  1. 设置浮点数的显示精度

浮点数精度的含义取决于输出模式。在默认情况下,指的是显示的总位数,在定点模式和科学模式下,精度指的是小数点后面的位数。c++默认精度位6位(末尾的0不显示)。precision()成员函数使得能选择其他值

1
2
//cout精度设置为2
cout.precision(2);
  1. 打印末尾的0和小数点

对于有些输出(比如价格栏中的数字),保留末尾的0将更为美观。

1
cout.setf(ios_base::showpoint);

上面的dec、hex、oct、left、right等都是标准控制符能够调用setf(),并且自动提供正确的参数,工作方式都相似。

头文件iomanip

使用iostream工具来设置一些格式值有时不太方便。为简化工作,C++在头文件iomanip中提供了一些控制符,作用类似,表示更方便。常用的有三个:setprecision()、setfill()和setw(),表示设置精度、填充字符、字段宽度。

setprecision():接受一个指定精度的整数参数

setfill():接受一个指定字段宽度的整数参数

setw():接受一个指定填充字符的char参数

因为他们都是控制符,故可以使用cout将语句连接起来。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
double root = 3.1415926;
for(int n=10;n<=100; n+=10)
{
cout << setw(6) << setfill('.') << n << setfill(' ');
cout << setw(12) << setprecision(3) << root << endl;
}
return 0;
}

使用cin进行输入

cin如何检查输入

不同版本的抽取运算符查看输入流的方法是相同的。他们跳过空白(空格、换行符、制表符),直到遇到非空白字符。即使对于单字符模式(char、unsigned char)也是如此。在其他模式下,>>运算符将读取一个指定类型的数据。将从非空白字符开始,到与目标类型不匹配的第一个字符的全部内容

1
2
3
4
int num;
cin >> num;
//假设输入12348z
cout << num;//输出12348,剩下的留在输入缓冲区

流状态

在C++中,cin 对象的流状态会受到输入的影响。cin 是C++标准库中的标准输入流对象,用于从标准输入设备(通常是键盘)获取用户输入。流状态的不同值反映了输入的有效性和状态。以下是一些关于 cin 流状态的详细介绍:

  1. 流状态位

cin 有一系列的流状态位,主要的有:

  • failbit 当输入的数据类型不匹配或输入格式错误时,failbit 会被设置。例如,用户输入了非整数字符而 cin 期望输入整数。
  • badbit 当输入流发生严重错误时,如IO错误或设备故障,badbit 会被设置。
  • eofbit 当遇到文件末尾时,eofbit 会被设置。
  • goodbit 表示没有错误发生。
  1. 流状态查询

可以通过 cin 对象的成员函数 fail()bad()eof()good() 来查询流的状态。

  • fail() 返回 true 表示 failbitbadbit 被设置。
  • bad() 返回 true 表示 badbit 被设置。
  • eof() 返回 true 表示 eofbit 被设置。
  • good() 返回 true 表示没有任何错误位被设置,即 goodbit 被设置。
  1. 流状态的影响

在使用 cin 进行输入时,输入的有效性和格式与流状态密切相关。如果输入不符合预期,流状态位将被设置,进而影响程序的行为。例如:

使用cin错误处理

在使用 cin 进行输入时,最好进行错误处理,以确保用户输入的数据是有效的。例如,如果用户输入了非整数的字符,cin 将进入错误状态。可以通过检查 cin.fail() 来检测错误,并通过 cin.clear() 来清除错误状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <limits>
using namespace std;

int main() {
int num;
cout << "Enter an integer: ";

while (!(cin >> num)) {
/*输入有效返回true,无效返回false*/
cout << "Invalid input. Please enter an integer: ";
cin.clear(); // 清除错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 忽略缓冲区中的无效字符
}

cout << "You entered: " << num << endl;

return 0;
}
1
2
3
4
5
- cin.clear() 的作用是清除 cin 对象的错误状态,以便能够继续尝试接收输入。如果不清除错误状态,cin 会一直保持在错误状态,导致后续的输入操作无法正常进行。

- cin.ignore(numeric_limits<streamsize>::max(), '\n') 的作用是清除输入缓冲区中的无效字符,直到遇到换行符为止。这通常用于处理用户输入错误时,清除缓冲区中的残留字符,以便下一次输入操作不受之前错误的影响。

- cin.fail() 是 cin 流的一个成员函数,用于检查最近一次的输入操作是否成功。它返回一个布尔值,如果最近的输入操作失败,则返回 true,否则返回 false

其他istream类方法

  1. 单字符输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cin.get(char& )
/*
在使用char参数或没有参数的情况下,get()方法读取下一个字符,即使该字符是空格、制表符或换行符。get(char& ch)版本将输入字符赋给其参数,而get(void)版本将输入字符转换为整型并将其返回
*/
cin.get(c1);

c1 = cin.get();//返回值为整型





cin.get(c1).get(c2) >> c3;
//可以进行拼接get,代表先赋值给c1返回调用对象cin,再赋值给c2,返回cin,最后把下一个非空白字符赋值给c3
1
2
3
4
5
6
7
8
9
10
11
12
13
int ct=0
char ch;
cin.get(ch);//如果这里替换位>>下面将不会退出循环
while(ch!='\n')
{
cout << ch;
ct++;
cin.get(ch);
}
cout << ct << endl;

//假设输入 I C++ clearly.<Enter>
//最终输出会跳过空格,输出IC++clearly.

2.字符串输入:get()、getline()和ignore()

  • cin.get()
1
2
3
4
5
6
7
8
istream & get(char*,int,char);
istream & get(char*,int);

- char*:放置输入字符串的内存单元的地址
- int:比读取的最大字符大1,额外的一个字符用于存储结果为空字符
- char:指定用作分界符的字符,只有两个参数时将使用换行符用作分界符

cin.get(array,100);

无参数时,读入一个字符,包括换行符,常用来处理输入缓冲区中的换行符。

有参数时,从缓冲区读取数据,到达行尾或size-1个字符**(剩下的空间储存在结尾添加的空字符)后结束读取(超过规定字符数不会出现错误,会直接截断),不会对换行符进行处理,将其留在输入流**。

  • cin.getline()
1
2
3
4
5
6
7
8
istream & getline(char*,int,char);
istream & getline(char*,int);

- char*:放置输入字符串的内存单元的地址
- int:比读取的最大字符大1,额外的一个字符用于存储结果为空字符
- char:指定用作分界符的字符,只有两个参数时将使用换行符用作分界符

cin.getline(array,100);

从缓冲区读取数据,到达行尾或size-1**(剩下的空间储存在结尾添加的空字符)**个字符结束读取(超过规定的字符数会出现错误,中断),会读取并丢弃输入流中的换行符。

  • ignore()
1
2
3
4
5
istream& ignore(int =1,int= EOF);

cin.ignore(255,'\n');

cin.ignore(255,'\n').ignore(255,'\n');//可拼接

原型为两个参数提供的默认值为1和EOF,EOF导致读取指定数目的字符或读取到文件结尾

cin.ignore 是 C++ 中用于忽略输入流中一定数量字符或特定字符的函数。这个函数通常用于清除输入缓冲区中的不需要的字符,以便在后续的输入操作中不受其影响。

文件输入输出

文件输入输出

iostream: 头文件中定义了一个处理输出的ostream类

fstream:头文件定义了一个用于处理输出的ofstream类

文件的输出主要步骤如下:

  1. 包含头文件fstream,iostream

  2. 创建一个ofstream(output fstream)对象(通常取名为outFile

  3. 将该ofstream 对象同一个文件关联起来(使用open方法)。

  4. 向cout那样使用ofstream对象(通常outFile)

    重点:cout在屏幕上输出,而outFile是在文件中输出(写入)

  5. outFile.close()关闭文件流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<fstream>
#include<iostream>//步骤1

int main()
{
int data;
ofstream outFile;//步骤2
outFile.open("data.txt",std::ios::out);//步骤3以写入的方式打开文件

if (!outFile.is_open())
{//判断文件是否打开成功,打开成功返回true
cout << "文件打开失败";
exit(1);
}

while()
{
outFile << data << endl;//步骤4,输入文件中
}

outFile.close();//步骤5,关闭文件流
}

文件的输入包括以下步骤

  1. 包含头文件iostream,包含头文件fstream

  2. 声明一个或多个ifstreaminput fstream)的变量(对象),并且命名,遵守常用的命名的规则,通常取名为inFile

  3. 将ifstream对象与文件关联起来。为此,方法之一是使用open()方法

  4. 可结合ifstream对象 和运算符>>来输入各种类型的数据。

  5. 使用inFile.close()关闭文件流

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

using namespace std;
int main()
{
int data;
char str[255];
ifstream inFile;//步骤2
inFile.open("student.txt",std::ios::in);//步骤3
if (!inFile.is_open())
{
cout << "文件打开失败";
exit(1);
}

while(!inFile.eof())//判断是否读取到文件结尾
{
inFile >> data;//步骤4
inFile.getline(str,255);
}

inFile.close();//步骤5
}

检查文件

格式为: 对象名.isopen() 例: inFile.is_open()

如果文件成功被打开,返回true;如果文件没有被打开,因此表达式 !inFile.is_open() 将为true

通常使用下方代码判断是否打开成功

1
2
3
4
5
6
7
8
9
if(!inFile.is_open())
{
cout<<"文件打开失败";
exit(1);
}

//exit(0)程序正常结束
//exit(1)程序异常结束
//exit()使用需要用到头文件cstdlib

判断文件结尾

格式为:对象名.eof() 例: inFile.eof()

eof在遇到EOF(文件结束标志)时返回ture ,否则返回false

通常和while循环一起用作文件读取结束,!inFile.eof在文件结束前为真,结束为假(退出循环)

具体格式为:

1
2
3
4
5
while(!inFile.eof())//判断是否读取到文件结尾
{
inFile >> data;//步骤4
inFile.getline(str,255);
}

文件打开关闭

  • 文件打开

**1.对象名.open(“文件名”) **(一个参数)例:

1
outFile.open("student.txt")

如果文件不存在,将会自动创建一个相同名字的文件,如果文件存在,将会打开该文件,首先截断该文件,将其长度截短到0,丢弃原有的内容,然后将新的输入加入到该文件中

2.对象名.open(“文件名”,打开方式) (两个参数)例如 :

1
outFile.open("student.txt",std::ios::in)

只读模式以只读模式打开

  • 文件关闭

格式:对象名.close() 例:

1
outFile.close()

注意方法close(),不需要使用文件名作为参数,因为outFile已经同特定的文件关联起来,如果忘记关闭文件,程序正常终止的时候将自动关闭它。

打开多个文件

打开多个文件时可以同时打开多个文件流:

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
int main()
{

std::ifstream userInFile,managerInFile;
userInFile.open("user_data.txt", std::ios::in);
managerInFile.open("manager_data.txt", std::ios::in);

if(!userInFile.is_open()||!managerInFile.is_open())
{
cout << "文件打开失败" << std::endl;
return false;
}

while(!userInFile.eof())
{
}

while(!managerInFile.eof())
{
}

userInFile.close();
managerInFile.close();
}

文件打开模式

C++ open 打开文件(含打开模式一览表)_c++ open函数-CSDN博客

c++新标准

更多内容见C++ primer plus 18章

统一的初始化

C++11扩大了大括号扩起的列表,即初始化列表的使用范围,使其可用于所有内置类型和用户定义的类对象。使用初始化列表时,可添加等号,也可不添加。

1
2
3
int x={5};
double y{2.75};
short quar[5] = {4,5,2,76,1};

另外初始化列表可用于new表达式中:

1
int* ar = new int[4] {2,4,6,7}//c++11

右值引用

左值和右值:

  • 左值(lvalue): 左值是可以标识内存位置的表达式。通常,左值是具有名称的变量、对象或表达式的结果,它们可以出现在等号的左边,可以被取地址。例如:

    1
    2
    int x = 42;  // x 是左值
    int* ptr = &x; // &x 是左值,因为它是地址
  • 右值(rvalue): 右值是不能标识内存位置的表达式。右值通常是临时的、无法取地址的值,出现在等号的右边。例如:

    1
    2
    3
    4
    int y = 10 + 5;  // 10 + 5 是右值
    int z = x + y; // x + y 是右值

    int z = x;//x不是右值,可以取地址

左值引用和右值引用:

  • 左值引用(lvalue reference): 左值引用是用于引用左值的引用类型。它使用 & 符号声明。左值引用主要用于在函数中传递参数或作为函数的返回类型,以及在赋值操作中。例如:

    1
    2
    int x = 42;
    int& ref = x; // ref 是对 x 的左值引用
  • 右值引用(rvalue reference): 右值引用是用于引用右值的引用类型。它使用 && 符号声明。右值引用通常与移动语义一起使用,允许有效地将资源从一个对象移动到另一个对象,而不进行深层复制。例如:

    1
    2
    int&& rref = 10 + 5;  // rref 是对右值的引用
    //无法对rref取地址

右值引用通常与移动语义结合,例如在移动构造函数和移动赋值运算符中使用,提高了对动态分配资源的效率。左值引用用于传递可修改的参数,而右值引用用于支持移动操作。

Lambda函数

在C++中,lambda 表达式是一种方便的方式,允许你在函数内部定义匿名函数。它的语法形式为:

1
2
3
4
[capture](parameters) -> return_type {
// lambda body
}

  • capture 是捕获列表,用于指定在 lambda 表达式中可以访问的外部变量。
  • parameters 是 lambda 函数的参数列表。
  • return_type 是返回类型。
  • lambda body 包含实际的函数体。

示例

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>

void operateWithLambda() {
int x = 5;
int y = 10;

// Lambda 表达式,接受两个参数,返回它们的和
auto add1 = [](int a, int b) -> int {
return a + b;
};

// 使用 Lambda 表达式计算并输出结果
int result1 = add1(x, y);
std::cout << "The sum1 is: " << result1 << std::endl;

// Lambda 表达式也可以访问外部变量
auto add2 = [x,y]() -> int {
return x+y;
};

int result2 = add2(x, y);
std::cout << "The sum2 is: " << result2 << std::endl;

auto addWithExternal = [x](int b) -> int {
return x + b;
};

// 使用 Lambda 表达式计算并输出结果
int resultWithExternal = addWithExternal(y);
std::cout << "The sum with external variable is: " << resultWithExternal << std::endl;
}

int main() {
operateWithLambda();

return 0;
}