C++ 基础知识学习

卡布叻_米菲 Lv2

这个笔记是我上课跟着老师讲解,并按照ppt上的内容进行整理和整合。实时更新ing~

C++

常识:C++之父:Bjarne Stroustrup

C++作为一门面向对象的语言,具有面向对象编程的众多特点:1、封装、继承和多态 2、作为抽象数据类型的类 3、易于调试和维护

封装

封装,即 隐藏对象的属性和实现细节,仅对外公开接口,控制程序对类属性的读取和修改。

  • 对于类的内部,成员函数可以自由修改成员变量,进行更精确的控制;
  • 对于类的外部,良好的封装能够减少耦合,同时隐藏实现细节。

抽象

  • 数据抽象:只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。
  • 过程抽象:是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

继承

即子类继承父类的特征和行为,使得子类具有父类的成员变量和方法。

多态

即同一个行为具有多个不同表现形式或形态的能力。表现形式有覆盖和重载。

类型

对象、引用、函数(包括函数模板特化)和表达式具有称为类型的性质,它限制了对这些实体所容许的操作,并给原本寻常的位序列提供了语义含义。

类型的分类

C++类型系统由以下类型组成:

  1. 基础类型
  • void 类型

  • std::nullptr_t 类型

  • 算数类型

    • bool 类型

      sizeof(bool) 的值由实现定义,而且不一定是 1

    • 字符类型:

      • 窄字符类型:
        • 通常字符类型(char、signed char、unsigned char)
        • char8_t 类型(C++20 起)
      • 宽字符类型(char16_t、char32_t、 (C++11 起)wchar_t)
    • 有符号整数类型:short int、int、long int、long long int (C++11 起)

    • 无符号整数类型:unsigned short int、unsigned int、unsigned long int、unsigned long long int (C++11 起)

    • 浮点类型:float、double、long double

  1. 复合类型
  • 引用类型:左值引用类型、右值引用类型
  • 指针类型:指向对象的指针类型、指向函数的指针类型
  • 指向成员的指针类型:指向数据成员的指针类型、指向成员函数的指针类型
  • 数组类型
  • 函数类型
  • 枚举类型
  • 类类型:非联合体类型、联合体类型

补充:标量类型是(可有 cv 限定的)算术、指针、成员指针、枚举和 std::nullptr_t (C++11 起) 类型

主函数

程序应当含有一个名字是 main 的全局函数(主函数),它被指定为程序的启动点。它应当有下列形式之一:

  • int main() { 函数体 }
  • int main(int argc, char* argv[]) { 函数体 }

在程序启动时,主函数在 初始化具有静态存储期的非局部对象 之后被调用。它是程序在有宿主 (hosted) 环境(即有操作系统)中所指定的入口点。自立程序(启动加载器,操作系统内核,等等)的入口点由实现定义。

主函数具有几项特殊性质:

  1. 不能在程序的任何地方使用它
    • 尤其不能递归调用它
    • 不能取它的地址
  2. 不能预定义,不能重载:实际上,名字 main 在全局命名空间中对函数保留(虽然可以用作类、命名空间、枚举和非全局命名空间中的任何实体的名字,但不能在任何命名空间中将名字是“main”的实体声明为具有 C 语言链接)。
  3. 不能定义为被弃置(=delete;),或 (C++11 起)声明为具有任何语言链接、constexpr (C++11 起)、consteval (C++20 起)、inline 或 static。
  4. 主函数的函数体不需要包含 return 语句:当控制达到主函数体的末尾而未遇到返回语句时,它的效果是执行 return 0;。
  5. 执行返回(或当到达主函数体的末尾时的隐式返回)与先正常离开函数(这将销毁具有自动存储期的对象),然后用和 return 相同的实参来调用 std::exit等价。(std::exit 随即销毁静态对象并终止程序)
  6. 主函数的返回类型不能被推导(不允许 auto main() {...})。

IO流

对象

  • 对象是现实世界或抽象世界中 事物的一种计算机表示。
  • 例如:猫tom
  • 面向对象语言中
    • 声明 Cat tom
    • 用 tom.color 访问颜色
    • 用 tom.catch() 让tom执行抓老鼠的行为

定义于头文件<iostream>三个对象

  • cout 是标准输出流对象,它可使用文件 stdout 输出
  • cin 是标准输入流对象,它可使用文件 stdin 输入
  • cerr 是标准错误流对象,它可使用文件 stderr 输出

运算符重载

  • C++ 绝大多数运算符都可以重载,即:
    • 根据操作数不同,自定义运算符的操作
  • 例如: >><< 是位移运算符,它左右操作数必须是整数
  • C++ 形象地将位移运算符用于流对象输入输出操作,例如:
    • cout << "hello" 的含义是将 hello 字符放入stdout 输出管道;
    • cin >> aInt 的含义是将 stdin 管道输入按"%d"格式转换后放入变量
  • << 重载为 输出运算
    • 左操作数必须是 输入流对象,右操作数是任意类型
    • 编译器会根据右操作数类型选择合适的 fprintf 格式化语句的版本。如右操作数是 double,输出格式是 "%lf"
    • 表达式返回左操作数的引用,是左值
  • >> 重载为 输入运算
    • 左操作数 必须是 输入流对象,右操作数必须是 左值,任意类型。
    • 编译器自动根据类型使用合适 fscanf
    • 表达式返回左操作数的引用,是左值

输入与输出运算符示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* iostream-basic.cpp */
#include<iostream>
using namespace std;

int main() {
int someInt;
float someFloat;
char someChar;
// fscanf(stdin,"%d%f%c", some...);
cin >> someInt >> someFloat >> someChar;
// fprintf(stdout,"the answer is: %f\n", some...);
cout << "the answer is: " << someInt * someFloat << endl;
cout << someChar << endl;
return 0;
}

运算符重载 不会改变 运算符的结合性和优先级。例如: 位移运算符是左结合,则 cin >> someInt 先计算并返回 左值 cin,再继续输出 someFloat,以此类推。

C 语言是 C++ 基础。要熟悉表达式的五个要素:类型、值类别、结合性、优先级、类型兼容与隐式转换

输入/输出操纵符(Manipulators)

  • 操纵符是让 输入/输出对象 控制格式化、或执行 文件操作 的特殊对象
  • 标头 <iostream> 定义了一些无参的操纵符,包括:
    • endl,输出'' 并冲洗输出流。例如:cout << endl;
    • dec, hex, oct, 更改用于整数输入/输出的基数(进制)
    • left, right, 设置填充字符的布置,即左对齐或右对齐
    • fixed, scientific,更改用于浮点 I/O 的格式化
    • showpoint, noshowpoint, 控制浮点表示是否始终包含小数点
    • showpos, noshowpos,控制是否将 + 号与非负数一同使用
  • 表头<iomanip> 定义了一些 有参的 操纵符函数,包括
    • setw(n) ,更改下个输入/输出域的宽度,宽度为 n
    • setprecision(n) ,更改浮点精度

示例:

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

int main()
{
double myFloat = 123.4567;
int myInt = 5;

cout << fixed << showpoint << setprecision(3);

cout << setw(10) << left << "Float";
cout << setw(12) << right << myFloat << endl;
cout << setw(10) << left << "Int";
cout << setw(12) << right << myInt << endl;

return 0;

}

补充:

<iomanip> 中常用的函数

控 制 符 作用
dec 设置整数为十进制
hex 设置整数为十六进制
oct 设置整数为八进制
setbase(n) 设置整数为n进制(n=8,10,16)
setfill(n) 设置字符填充,c可以是字符常或字符变量
setprecision(n) 设置浮点数的有效数字为n位
setw(n) 设置字段宽度为n位
setiosflags(ios::fixed) 设置浮点数以固定的小数位数显示
setiosflags(ios::scientific) 设置浮点数以科学计数法表示
setiosflags(ios::left) 输出左对齐
setiosflags(ios::right) 输出右对齐
setiosflags(ios::skipws) 忽略前导空格
setiosflags(ios::showpos) 输出正数时显示"+"号
setiosflags(ios::showpoint) 强制显示小数点
resetiosflags 清除指定的 ios_base 标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
double PI = 3.141592654;
cout << PI << endl;
cout << setprecision(2) << PI << endl;
cout << fixed << setprecision(2) << PI << endl;
cout << setfill('*') << setw(20) << setprecision(10) << PI << endl;
cout << setfill('v') << setw(20) << left << PI << endl;
cout << scientific << setprecision(10) << PI << endl;
cout << scientific << uppercase << setprecision(10) << PI << endl;

int num = 123;
cout << hex << num << endl; // 输出十六进制数
cout << resetiosflags(ios::hex); // 清除十六进制标志
cout << num << endl; // 输出十进制数

return 0;
}

命名空间

C语言中,每个应用中全局标识符仅能定义一次

C++命名空间提供了一种在大项目中避免名字冲突的方法

  • 在命名空间块内声明的符号被放入一个具名的作用域中,避免这些符号被误认为其他作用域中的同名符号。

  • 多个命名空间块的名字可以相同。这些块中的所有声明在该具名作用域声明。

  • 例如:标准库中符号(类型、变量、常量、函数等)都在 std 命名空间块中声明。cout 是在 std 命名空间块中声明,则

    • std::cout 则是该变量的有限定名
    • :: 是作用域解析运算符
  • 声明具名命名空间

    namespace 命名空间名{声明序列}

  • 使用其他命名空间的名字

    • 以 "无限定名"方式使用:直接从 using 指令之后到指令出现的作用域结尾为止,以对任何无限定名字,来自指定命名空间的成员名或指定成员均可见。

      using namespace 命名空间名

      using 命名空间名::成员名

    • 以"有限定名" 方式使用:随着IDE进步,直接指定使用命名空间中成员更常用。 命名空间名::成员们

不同命名空间

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

// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{

// 调用第一个命名空间中的函数
first_space::func(); //输出 Inside first_space

// 调用第二个命名空间中的函数
second_space::func(); //输出 Inside second_space

return 0;
}

命名冲突示例:

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

int main(){
const char* cout = "hello world c++" << endl;
std::cout << cout << endl;
return 0;
}
  • using namespace std; 表示可使用 std 命名空间内成员名称的指令
  • 但 局部变量 cout 隐藏 std 空间中的定义
  • std::cout 是标准输出流对象有限定名称,避免了字符串变量cout的命名冲突
  • 注意:C++比C严格,const指针值必须赋给const指针变量。

命名空间嵌套示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* ns-embed.cpp */
#include<iostream>
using namespace std;

namespace sysu {
namespace students {
int collegeCount;
void printColleges();
}
}

void sysu::students::printColleges() {
cout << "Colleges " << collegeCount << endl;
}
int main() {
using namespace sysu::students;
collegeCount = 23;
printColleges();
return 0;
}
  • 声明命名空间及其成员
  • 定义空间中成员函数

注意: 两个using作用域不同

课堂题:创建函数 : void Handle();

在函数中实现如下功能:

  • 命名空间mfc中的变量inflag进行操作: inflag++;
  • 命名空间owl中的变量inflag进行操作: inflag*2;
  • 全局变量inflaginflag mod 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
----------NameSpace.hpp-------------
#ifndef Namespace
#define Namespace

extern int inflag ;

namespace mfc {
extern int inflag;
}

namespace owl {
extern int inflag;
}
void Handle();
#endif
------------Hanle.cpp----------------
#include"NameSpace.hpp"

void Handle()
{
::inflag %= 2; //全局变量直接在前面加 ::
(mfc::inflag)++;
(owl::inflag) *= 2;
}

引用

应用的概念:

  • 声明具名的变量引用,即已有对象或函数的别名
  • 应用的特定:使用时类似变量,常作为参数时传引用

引入原因:指针强大、灵活,但是程序容易出BUG,难调试

引入可以减少指针使用,甚至替代指针。

应用应该在声明的同时初始化。

引用的值不能为null

左值声明与示例

左值引用的声明:type &别名[= 左值表达式]

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

int main(){
int a = 1024;
int* p = &a; // p 是 指针,&a 是a的地址
int &x = a; // x 是 引用,它实际上与 a 是同一个变量
cout << "a = " << a << endl;
cout << "x = " << x << endl;
cout << "*p = " << *p << endl;
x = 2000;
cout << "a = " << a << endl;

return 0;
}

引用不是对象,它们不必占用存储

因为引用 不是对象,故:

  • 不存在引用的数组
  • 不存在指向引用的指针
  • 不存在引用的引用

两种引用形式

  • 左值引用声明符:声明 S& D; 是将 D 声明为 声明说明符序列 S 所确定的类型的左值引用
  • 右值引用声明符:声明 S&& D; 是将 D 声明为 声明说明符序列 S 所确定的类型的右值引用

引用的详解

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//程序RefSwap
void swap(int &x, int &y)
{
    int temp;
    temp = x;
    x = y;
    y = temp;
}
int main()
{
    int a, b;
    cin >> a >> b;
    swap(a, b);
    cout << "max = " << a<< " "<< "min = " << b;
}

提示:c 编译实现采用将a,b的地址作为值传递给函数 swap,但官方并没有定义引用是值为地址的变量,就是变量的别名。这样以保证与其他高级语言的引用概念一致。

指针与引用

  • 相同:可以使一个函数向调用者返回多个数值
  • 不同:原理不同
    • 引用传递中,形参、实参实质为同一变量,或者说是为某个变量起多了一个名字。
    • 使用指针作为函数参数,则是被调用函数获得某变量的地址,从而使用这个地址访问这个变量。
  • 从返回值的角度,引用形参比指针方便

返回值非引用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double f( double x )     
{
double y;

y = sin(x);

return y;
}
int main()
{
double a = 3.14/6;
double y;

y = f( a );

cout << "y = " << y << endl;

return 0;
}

image-20230223160434402

使用引用的返回值示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double& f( double x ) //程序ref2
{
static double y;

y = sin(x);

return y;
}
int main()
{
double a = 3.14/6;
double y;

y = f( a );

cout << "y = " << y << endl;

return 0;
}

image-20230223160619812

注:引用型返回值,不能返回 auto变量

左值引用:

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

int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值

const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延长生存期
// r2 += "Test"; // 错误:不能通过到 const 的引用修改

std::string&& r3 = s1 + s1; // OK:右值引用延长生存期
r3 += "Test"; // OK:能通过到非 const 的引用修改
std::cout << r3 << '\n';
}

转发引用

转发引用(Forwarding Reference)是C++11中引入的一种特殊的引用类型,也称为“完美转发”(Perfect Forwarding)。它使用双引号(&&)表示,可以将一个参数以原样传递给另一个函数,而不会丢失其值类别(左值或右值)。

转发引用是一种特殊的引用,它保持函数参数的值类别,使得 std::forward 能用来转发参数。转化引用是下列之一:

  1. 函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无cv限定的右值引用 转发引用通常用于模板函数中,以便在不知道参数类型的情况下,将参数转发给其他函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<class T>
    int f(T&& x) //x 是转发引用
    {
    return g(std::forward<T>(x));
    }

    int main()
    {
    int i;
    f(i);//实参是左值,调用 f<int&>, std::forward<int&>(x) 是左值
    f(0);//实参是右值,调用 f<int>(int&&), std::forward<int>(x) 是右值
    }

    需要注意的是,转发引用只能用于函数模板中,而不能用于普通函数或成员函数。

    1. auto&&,但当其从花括号包围的初始化器列表推导时除外:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    auto&& vec = foo();       // foo() 可以是左值或右值,vec 是转发引用
    auto i = std::begin(vec); // 也可以
    (*i)++; // 也可以

    g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别

    for (auto&& x: f())
    {
    // x 是转发引用;这是使用范围 for 循环最安全的方式
    }

    auto&& z = {1, 2, 3}; // *不是*转发引用(初始化器列表的特殊情形)

悬垂引用

尽管引用一旦初始化就始终指代一个有效的对象或函数,但有可能创建一个程序,其中被指代对象的生存期 结束而引用仍保持可访问(悬垂(dangling))。访问这种引用是未定义行为。 一个常见例子是返回自动变量的引用的函数:

1
2
3
4
5
6
7
8
9
std::string& f()
{
std::string s = "Example";
return s; // 退出 s 的作用域:调用其析构函数并解分配其存储
}

std::string& r = f(); // 悬垂引用
std::cout << r; // 未定义行为:从悬垂引用读取
std::string s = f(); // 未定义行为:从悬垂引用复制初始化

注意,右值引用和到 const 的左值引用能延长临时对象的生存期。

如果被指代对象被销毁(例如通过显式的析构函数调用),但存储尚未被解分配,则到生存期外的对象的引用仍能以有限的方式使用,且当在同一存储中重新创建对象时也可以变为有效

传值与传引用

C++的函数参数的传递方式,可以是

  • 传值方式。传值的本质是:形参是实参的一份拷贝
  • 传引用的方式。传引用的本质是:形参和实参是同一个对象
1
2
3
4
void fun_1(int a); //int类型,传值(复制产生新变量)
void fun_2(int& a); //int类型,传引用(形参和实参是同一个东西)
void fun_3(int* arr); //指针类型,传值(复制产生新变量)
void fun_4(int*& arr); //指针类型,传引用(形参和实参是同一个东西)

常 (const 限定) 引用

普通引用:int &a = b

  • 可以理解为:int* const a = &b
  • 即引用是一个指针常量(又称常指针,即一个常量,其类型是指针)
  • 每当编译器遇到引用变量a,就会自动执行*操作

常引用:const int& a = b

  • 就相当于:const int* const a = &b
  • 不仅仅是 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* FIRST 引用普通变量 */
int const_ref_without_casting() {
int b = 10;
const int &a = b;
// a = 12; //error: assignment of read-only reference
b = 11; //这可以改!
printf("a=%d,b=%d\n", a, b);
}
/* SECOND 引用字面量-实际引用临时变量 */
int const_ref_with_constant() {
const int &c = 15;
//编译器会给常量15开辟一片内存,并将引用名作为这片内存的别名
// int &d=15; //error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
int* p = (int *)&c; //const转非const必须显式
*p = 10; //修改了为15开辟的内存内容
cout << c << endl;
}
/* THIRD 引用表达式-实际引用临时变量 */
int const_ref_with_casting() {
double b = 3.14;
const int &a = b; // equal to: int temp = b; const int &a = temp
b = 11;
printf("a=%d,b=%f\n", a, b);
}
/* FOURTH 常引用参数-不能改变实参 */
// const 限定引用参数
int const_ref_in_para(const string& s) {
cout << s << endl;
}
/* FIFTH 引用参数-可以改变实参 */
int no_const_ref_in_para(string& s) {
cout << s << endl;
}
/* SIXTH */
int main()
{
const_ref_without_casting();
const_ref_with_constant();
const_ref_with_casting();
const_ref_in_para(“I love c++”); //类型匹配
//no_const_ref_in_para(“I love c++”); //error:类型不匹配
}

常变量、右值引用

const 常量(常变量)的特性

  • 特性1:是变量。从而可以通过指向该常变量的指针改变常变量的值。
  • 特性2:是常量。直接使用这个常变量的名字,仍然是原来的常量值。

右值和右值引用(&&)

  • 在C++11中可以取地址的、有名字的就是左值
  • 反之,不能取地址的、没有名字的就是 右值。(简单理解:右值是左值表达式)
  • 右值引用就是 对右值的引用,即对存储右值的临时变量的引用。
  • 右值引用和左值引用 都是 左值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
Using namespace std;
Int main()
{
const double pi = 3.14; //编译:像普通变量那样,分配空间
// double *r_pi = π //语法错误?
double *p_pi = (double *)π//常变量特性1:变量特性,可取地址
*p_pi = 4.0;

printf(“%lf,%lf\n”,*p_pi,pi); //常变量特性2:常量特性(编译优化:确保原来的常量值)
// double &r_pi = pi; //语法错误?
const double &r_pi = pi; //引用const常量pi,无法修改
// r_pi = 4.0; //语法错误?
double &&rr_pi = (double)pi; //右值引用,可修改
rr_pi = 5.0;
//请解释输出结果
printf("%lf,%lf,%lf,%lf\n",*p_pi,r_pi,rr_pi,pi);
}
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
class Test {
public:
void Test1(int _a) const { //常方法
std::cout<<"Test()const"<<std::endl;
a = _a; //error 常方法不能修改普通成员变量的值
int d = _a; //可以访问a,但不能修改a的值
int e = c; // const函数访问const成员变量
}
void Test2() {
int mf = c; //普通函数访问const成员变量
std::cout<< "Test2(int,int)" <<std::endl; //输出该函数
}
Test(int _a,int _b,int _c):a(_a),b(_b),c(_c){
//构造函数(常成员可由初始化表进行初始化)
std::cout<< "Test(int)" <<std::endl; //输出该函数
}
private:
int a, b;
const int c; //常数据成员。初始方式1:默认初始化;初始方式2:构造函数的初始化表
};

int main()
{
Test tmp(10,20,30); //调用构造函数生成对象tmp
tmp.Test1(10); //const函数访问常数据成员变量
tmp.Test2(); //普通函数访问常数据成员变量
return 0;
}

常数据成员:

  1. 初始方式1:默认初始化;初始化方式2:构造函数的初始化表
  2. 可以成员函数访问(但不允许修改期中的值)

常对象:常对象只能调用该类中的常成员函数(ji)

  1. 可以访问对象中的任何数据成员,但不能修改
  2. 可以调用常方法,不能调用不是常方法的其他成员函数

数据存储

  1. 代码段 / 文本段(code segment / text segment)

    • 作用:存放指令,运行代码的一块内存空间,存储 函数实现,库实现,字符串等资源,不可改。
    • 该区域的大小在程序运行前就已经确定
    • 内存空间一般属于只读,某些架构的代码也允许可写
    • 在代码段中,也可能包含一些只读的常数变量,例如 字符串常量
  2. 全局区/静态数据存储区域

    1. 数据段(data segment)

      • 可读可写

      • 存储初始化的全局变量和初始化的static变量

      • 数据段中数据的生存期是随程序持续性(随进程持续性)

        进程持续性:进程创建就存在,进程结束就消失

    2. bss段(bss segment) bss : block start by symbol

      • 可读可写
      • 存储未初始化的全局变量和未初始化的 static 变量
      • bss段中数据的生存期随进程持续性
      • bss段中的数据一般默认为 0
  3. rodata段

    • 只读数据
    • 比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如:作用域中的 const int ival = 10,ival 存放在rodata 段;再如,函数局部作用域中的 printf("Hello world %d", c); 语句中的格式字符串"Hello world %d"也存放在 rodata段
  4. 栈(stack)

    • 可读可写
    • 存储的是函数参数或代码段中的局部变量(非 static 变量)
    • 栈的生存期随代码块持续性,代码块运行就分配空间,代码块结束就自动回收空间
  5. 堆(heap)

    • 可读可写
    • 动态变量(对象), 由 stdlib.h 管理

image-20230323142945866

1
2
3
4
char* s1 = "Literal"; //文字在代码区,仅分配了字符指针
// s1[0] = 'I'; //Segmentation fault
char s2[] = "Initial Literal"; //分配数组空间
Point po = Point{2,3}; //分配结构空间
  • 在函数外声明,或者用 static 修饰,则会存储在全局区/静态变量存储区
  • 在函数内声明,则在 栈 中分配空间,并初始化

C动态对象(变量)管理

堆(Heap)

  • 共享的对象(变量)空间
  • 由 stdlib 库管理

正确使用堆空间

  • 必须 #include<stdlib.h>
  • 申请空间,void* malloc(size_t)
  • 释放对象,void free(void*)
    • 申请的空间 必须释放 ,否则就是内存泄漏
    • free 后再使用指针或释放,行为不可预测❌

C++动态对象(变量)管理

C++ 用 malloc 分配对象数组

程序要点

  • #include<cstdlib>
  • if 语句判断空间是否分配
  • 注意 malloc 不会调用对象的构造函数,free 不会调用析构函数
  • 初始化对象
    • new(void*) 构造函数
  • 处理构造异常
  • 显式调用析构函数
    • ACLASS.~析构函数
  • 最后释放空间
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<cstdlib>
#include<string>
int main(){
//为 4 个 string 的数组分配足够空间
if(auto p = (std::string*)std::malloc(4 * sizeof(std::string))){
int i = 0;
try{
for(; i!= 4; i++){
new(p+i) std::string(5, 'a'+i);
}

for(int j = 0; j!=4; j++){
std::cout << "p["<< j <<"] == "<< p[j] << std::endl;
}

catch(...){}
for(; i!=0; i--){
p[i-1].~basic_string();
}

std::free(p);
}
}
}

C++新关键字:new 和 delete 运算符

新关键字优点:

  • new 类型 初始化
    • 分配空间
    • 每个对象调用构造函数
    • 有错误抛出异常
  • delete[] p
    • 为数组每个对象调用析构函数
    • 释放空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*new-delete-strings*/
#include<isotream>
#include<string>
using namespace std;
int main(){
//为 n 个 string 的数组分配空间
int n = 4;
string* p = new string[n]{
string(5, 'a'),
string(5, 'b'),
string(5, 'c'),
string(5, 'd')
};

for(int i = 0; i<n; i++){
cout << p[i] << endl;
}

delete[] p;
}

动态对象(变量)与对象指针

动态对象的使用

FIRST-创建单个基本数据类型

  • 创建:new 类型 ,返回该类型的指针,指向新建对象
  • 使用 *p 解析为对象
  • 释放:delete 指针
1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
int main(){
int* p;
p = new int;
cout << "Please enter an integer value:";
cin >> *p;
cout << "The value you enter is:" << *p;

delete p;
}

SECOND-创建数组

  • 创建:new 类型[]{初始化列表};
    • 和定义自动变量类似。没有初始化列表则调用无参构造(默认构造一般式不确定值);常数初始化列表,后面补零;长度小于初始化列表的长度,抛出异常
    • 返回该类型的指针
  • 直接使用 p 直接当作数组名使用
  • 释放:delete[] 指针
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;
void inc(int* p, int length){
for(int i = 0; i<length; i++){
p[i]++;
}
}
int main(){
int* p;
int length, i;
cout << "Enter the length you want:";
cin >> length;
p = new int[length]{1,2,3};

inc(p, length);

for(i = 0; i<length; i++)
cout << *(p+i) <<" ";

delete[] p;
}

new 运算符的使用

指针 = new 类型名; //动态创建一个对象

指针 = new 类型名() //动态创建一个对象

指针 = new 类型名[数组长度]; //用于动态分配数组

  • 初始化参数及其括号为可选
  • 类型可为基本类型,也可为 类 类型
    • 若为 类 类型, 则初始化参数相当于将 实际参数传递给该类的构造函数
  • new 运算返回一个该类型指针(不是 void*),指向分配到的内存空间
  • 若内存分配失败,抛出异常,不是NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new int(*[10])();    // 错误:解析成 (new int) (*[10]) ()
new (int (*[10])()); // OK:分配 10 个函数指针的数组

new int + 1; // OK:解析成 (new int) + 1,增加 new int 所返回的指针
new int * 1; // 错误:解析成 (new int*) (1)

double* p = new double[]{1, 2, 3}; // 创建 double[3] 类型的数组
auto p = new auto('c'); // 创建单个 char 类型的对象。p 是一个 char*

auto q = new std::integral auto(1); // OK: q 是一个 int*
auto q = new std::floating_point auto(true) // 错误:不满足类型约束

auto r = new std::pair(1, true); // OK: r 是一个 std::pair<int, bool>*
auto r = new std::vector; // 错误:无法推导元素类型

int n = 42;
double a[n][5]; // 错误
auto p1 = new double[n][5]; // OK
auto p2 = new double[5][n]; // 错误:只有第一维可以不是常量
auto p3 = new (double[n][5]); // 错误:语法 (1) 不能用于动态数组

delete 操作符的使用

delete 变量名; //基本用法

delete[] 变量名; //用于释放数组

  • 如果动态分配了一个数组,但是却用 delete p 的方式释放,没有用[],则
    • 编译时没有问题,运行时也一般不会发生错误
    • 但实际上会导致动态分配的数组没有被完全释放
  • delete 释放的是指针所指对象占据的内存
  • 用 delete 释放空间后,指针的值仍然是原来指向的地址,但指针已无效(重复释放将出错)
  • delete 对象指针,会调用对象的析构函数

内存泄露

内存泄漏:指 new 的空间失去了指针或引用永远无法释放。导致资源耗尽!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//case 1
int* p = new int(7); // 动态分配的 int 带值 7
p = nullptr; // 内存泄漏

//case 2
void f()
{
int* p = new int(7);
} // 内存泄漏

//case 3
void f()
{
int* p = new int(7);
g(); // 可能抛出异常
delete p; // 如果没有异常则 ok
} // 如果 g() 抛出异常则内存泄漏

string 类

类(class):是关于同类事物定义的描述。

在面向对象语言中

  • 类是对对象的描述,描述对象的属性和行为。
  • 类是模板,它是关于其对象的数据成员和函数成员的描述。
  • class 是一种数据类型

在标准库中有很多类,如以下常用的类

  • <string>库中的 std::string
  • 输入std::ifstream, std::ofstream

C++提供了以下两种类型的字符串表示形式:

  • C风格字符串 C语言风格字符串是使用null字符'\0' 终止的一维字符数组,需要事先要知道保留多大空间存储字符串,字符串操作strcat要保证目标字符串有足够空间。要用malloc维字符串动态分配空间。
  • C++的 string 类 类型的对象

创建 string 对象

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

int main()
{
string s1;
string s2 = "c++";
string s3 = s2;
string s4(5, 's');

cout << s3 << endl;
return 0;
}
  1. s1 未使用初始化参数,即默认初始化为空字符串。
  2. 对象类型 对象=参数1,表示用参数 1 初始化对象。 s2 使用 C字符串初始化,s3 用 s2 初始化(又称拷贝复制)
  3. 对象类型 对象(参数列表) 适用于有参数的对象初始化。 s4 初始化为 5 个 s
  4. string s2 = "c++"不是赋值运算,它等价于 string s2("C++") 是初始化。

string 对象的运算

string 类实现以下 运算符的重载

  • 联接运算 operator+,连接两个字符串或者一个字符串和一个字符

  • 比较运算

    operator==, !=, <, <= , > , >= ,<=>, 以字典序比较两个字符串

  • 下标运算

    operator[], 访问指定字符,返回指定字符引用,即可作为左值

  • 赋值运算

    operator=, +=,右操作数可以是字符串,字符,C字符串,字符数组

案例:

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

int main()
{
string s1 = "hello", s2, s3;
cin >> s2;
s3 = s1+s2;
cout << s3 << endl;

s1 += {'s', 'y', 's', 'u'};
cout << s1 << endl;

return 0;
}

string类实现了许多 string 对象的操作(成员函数)

  • 基本操作:length,c_str

    length() 返回类型为 size_t 的字符串的长度

    c_str() 返回类型 const char* 的 C 风格字符串(以null字符结束)

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

    int main()
    {
    string s = "sysu-computer";
    int len = s.length();
    const char* cs = s.c_str();
    cout << cs << "len is" << len << endl;

    return 0;
    }
  • 查找:find, rfind find(string | char* | char s[, int pos = 0])寻找首个等于给定字符序列的子串。搜索始于 pos,返回类型 size_t 的位置。rfind 从右边开始搜索。

  • 添加,插入,删除:append, insert, erase, clear

    append(string) 后附 string str

    append(int count, char ch) 后附 count 个字符 ch

    insert(int index, int count, char ch) 在 index 位置插入 count 个 ch

    erase(int index, int count) 删除从 index 开始的 count 个字符

    clear() 清空

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

    int main()
    {
    string s = "ysu";
    s.insert(0,1,"S").append(1,'-').append("Computer");
    s.erase(5,1);
    cout << s << endl;

    return 0;
    }
  • 提取子字符串:substr

    substr(int pos, int count) 返回字串对象

  • 比较:compare

    compare(string|char* str) 与 str 比较,返回 1, 0, -1 之一

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

    int main()
    {
    string s = "Sysu-Computer";
    cout << s.substr(0,4) << " ";
    if(s.compare(5,8,"Computer") == 0)
    {
    cout << "OK!" << endl;
    }
    return 0;
    }
  • 替换:replace

    replace(int pos, int count, string|char*|char s) 替换指定范围的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* str-method-find.cpp */
    #include<iostream>
    #include<string>
    using namespace std;

    int main() {
    string s = "sysu-computer";
    int pos = s.find("computer");
    s.replace(pos,8,"software");
    cout << s << endl;
    return 0;
    }
  • 复制与交换:copy, swap

    copy(string|char* dest, int pos, int count) 将子串复制到目标对象

    swap(string other) 与other交换内容

  • 数值转换(C++11):stoi, stod

函数重载

​ 在C语言中,一个函数只能定义一个函数名以及相关形参。C++则允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载。

借助重载,一个函数名可以有多中用途。

使用C标准库

C++标准库提供了可以在C++中使用的各类基础设施。也包括:

  • C库基础设施的C++标头,即在 std 空间中的 c标准库
  • C标准库,即不仅可以用 g++编译C风格的cpp程序,也可以使用C库函数

const 和 constexpr(C++)

C语言中 const 关键字限定变量不可修改。

const有两个语义:

  • 限定为 不可修改(只读)变量
  • 限定作为常量或字面量

C++11 标准添加关键字 constexpr,声明编译时可以对函数或变量求值。即

  • 限定为常量表达式
  • 限定为编译时可优化执行的函数

C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是

  • 表达“只读”语义的场景都使用 const
  • 表达“常量”语义的场景都使用 constexpr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <array>
using namespace std;
void dis_1(const int x){
    //错误,x是只读的变量
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}
void dis_2(){
    const int x = 5;
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}
int main()
{
   dis_1(5);
   dis_2();
}

dis_1() 函数中的“const int x”只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;而 dis_2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。

数据抽象和类

Data abstraction:只关心该数据“是什么”以及“如何使用”,而不关心它是如何运作的。

Control abstraction:只关心这个行为能为我们带来什么,而不关心这个行为的具体实现方法。

抽象数据类型

在程序设计中,通过数据抽象而获得的抽象数据,称为抽象数据类型(Abstract Data Type, ADT)。

一种 ADT 应具有:

  1. 说明部分:要说明该 ADT 是什么及如何使用,从而描述:

    • 数据值的特性 和
    • 作用于这些数据之上的操作

    ADT 的用户仅须明白这些说明,而无须知晓其内部实现。

  2. 实现部分。

把 DATE 设计为一种数据类型。

  • 内部包含年月日等数据以及在这些数据上可进行的操作。

好处:

  • 用户利用 DATE 就可以定义多个变量。
  • 用户可调用每个变量中公开的操作,但无法直接访问每个变量中隐藏的内部数据。
  • 用户也无需关心变量中各操作的具体实现。

效果:于是 DATE 就是一种封装好的数据类型。这就达到信息隐藏和封装的目的。

1
2
3
4
5
6
7
8
9
10
Type
DATE
Data
Each DATE value is date
int day/month/year // ---> int year, month, day;
Operation
Set the date //set()
Get the date //get()
Increment the date by one day //increment()
Decrement the date by one day //decrement()

显示默认的函数定义: =default

令编译器为 某个类 生成 默认的特殊成员函数 或 比较运算符 的显式指令。(C++)

  • 例:当我们声明有参构造函数时,编译器就不会 创建 默认构造函数。为了使编译器创建该默认构造函数,可以

    • 在函数声明后指定 = default
    • 若以 = default 声明,则该函数不能写实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include<iostream>
    using namespace std;
    class A{
    public:
    A(int x){
    cout << "有参构造";
    }
    A() = default;
    };

    int main(){
    A a; //call A()
    A x(1); //call A(int x)
    cout << endl;
    return 0;
    }
  • default函数只能用于特殊的成员函数,特殊成员函数 包括:

    • 默认构造函数
    • 析构函数
    • 复制构造函数

弃置函数:=delete

如果取代函数体而使用特殊语法= delete,则该函数被定义为 弃置的(deleted)。

任何弃置函数的使用都是非法的(程序无法编译)。这包括

  • 调用,包含显示(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用)
  • 定义指向弃置函数的指针或成员指针
  • 甚或是在不求值表达式中使用弃置函数

请注意,删除的函数是 隐式内联 的,这一点非常重要。删除的函数定义 必须 是函数的 首次声明

✔正确用法✔

1
2
3
4
class C{
public:
C(C& a) = delete;
};

❌错误用法❌

1
2
3
4
5
6
7
// incorrect syntax of declaring a member function as deleted 
class C {
public:
C();
};
// Error, the deleted definition of function C must be the first declaration of the function.
C::C() = delete;

C++在C语言的基础上增加了面向对象编程,C++支持面向对象程序设计。类是C++的核心特性,通常被称为用户定义的类型。类用于指定对象的形式,它包含数据的表示方法和用于处理数据的方法。

  • 类中数据和方法 称为 类的成员
  • 函数 在一个类中被称为 类的成员

定义一个类,其效果是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

类的例子

类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中类定义后必须跟着一个分号或一个声明列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DATE{
public:
void Set(int, int, int);
int getMonth() const;
int getDay() const;
int getYear() const;
void Print() const;
void Increment();
void Decrement();
private:
int month;
int day;
int year;
};

在{}中列出类的成员。类的成员包括:

  • 数据成员:
    • 一般来说,数据成员是需要隐藏的;即外部的程序是不能直接访问这些数据的,应该通过函数成员访问这些数据。
    • 所以一般情况下,数据成员通过关键字 private 声明为私有成员 (private member)
  • 函数成员:
    • 通过关键字 public 声明访问 公有成员(public member)
    • 外部程序可以访问公有成员,但是无法访问私有成员
  • 对于 类的使用者(即用户代码,简称用户)而言
    • 只需要获得 .hpp ,即可调用类对象的公有函数访问其内部的数据成员
    • 使用者无法直接访问私有成员,也无需知晓公有函数的内部实现

结构体与类

区别:

  • C 语言中的 struct 只能包含变量
  • C++中的 class 除了包含变量,还可以包含函数

效果: 例如 set() 函数处理成员变量的函数

  • 在C语言中,我们将它放在了 struct Date 外面,它和成员变量时分离的
  • 在C++中,我们将它放在了class Date 内部,它和成员变量聚集在一起,看起来像一个整体

相似之处:

  • 结构体和类都可以看作一种由用户自己定义的复杂数据类型
  • 在C语言中可以通过结构体名来定义变量
  • 在C++中可以通过类名来定义变量

不同之处:

  • 通过结构体定义出来的变量传统上叫变量
  • 通过类定义出来的变量有了新的名称,叫做 对象(Object)

成员函数

声明:

  • 类的成员函数是指哪些把定义/原型写在类定义内部的函数,就像类定义的其他变量一样
  • 类成员函数是类的一个成员,它
    • 可以操作类的任意对象
    • 可以访问对象中的所有成员

定义:

成员函数可以

  • 在类体内部定义
  • 单独使用 范围解析运算符 :: 来定义。

在类体中定义的 成员函数 把函数声明为 内联 的,即便没有使用 inline 标识符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DATE { 
public:
void Set(int newYear, int newMonth, int newDay ) {
month = newMonth;
day = newDay;
year = newYear;
}
……
private:
……
};

/***************** 外部定义 *****************/
class DATE {
public:
void Set(int newYear, int newMonth, int newDay );
……
private:
……
};
void DATE::Set(int newYear, int newMonth, int newDay ) {
month = newMonth;
day = newDay;
year = newYear;
}

成员函数 作用域

  • 在DATE.cpp文件开头需要加入预处理命令#include "DATE.hpp"

    这是因为在DATE.cpp中要用到用户自定义的标识符DATE,而它的定义在DATE.hpp中。

  • 在DATE.hpp中,各函数原型是在{}中的。根据标识符的作用域规则,它们的作用范围仅在类定义中,而不包括DATE.cpp。因此在DATE.cpp中需要利用作用域解释运算符“::”来指明这里的函数是类DATE里的成员函数。

  • DATE.cpp中有时还包括DATE内部要使用到的函数,例如DaysInMonth(…)。这种函数并非对外公开供用户使用,因此可以将其声明为类的私有成员。

  • 若在该函数中没有涉及该类的非静态成员,则无需将它们声明为类的成员。

访问成员

调用 成员函数 和 成员变量 是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据

1
2
date.flag = 1;
date.set(2022, 1, 30);

静态成员

静态(static)成员是类的组成部分但不是任何对象的组成部分

定义方式:通过在成员声明前加上保留字static 将成员设为static

  • 在数据成员的类型前加保留字 static 声明 静态数据成员
  • 在成员函数的返回类型前加保留字 static 声明静态成员函数

static 成员遵循正常的公有/私有访问规则

C++程序中,如果访问控制允许的话,可在类作用域外直接访问静态成员

静态数据成员

  • 静态(static)数据成员 具有 静态生产期,是类的所有对象共享的存储空间,是整个类的所有对象的属性,而不是某个对象的属性。
  • 与非静态数据成员不同,静态数据成员不是通过构造函数进行初始化,而是必须在类定义体的外部再定义一次,且有且只有一次
    • 通常是在 类的实现文件中再 定义一次,而且此时不能再用 staic 修饰

静态成员函数

  • 静态成员函数不属于任何对象
  • 静态成员函数没有 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
28
29
30
31
32
33
34
35
36
37
38
class DATE // DATE2.hpp
{
public:
DATE( int =2000, int =1, int = 1);
static void getCount( );
void Set( int, int, int);
int getMonth() const;
int getDay() const;
int getYear() const;
void Print() const;
void Increment();
void Decrement();
private:
int month;
int day;
int year;
static int count;
};

//DATE2.cpp StaticMember
int DATE::count = 0; //必须在类定义体的外部再定义一次
DATE::DATE( int initYear, int initMonth, int initDay )
{
year = initYear;
month = initMonth;
day = initDay;
count++;
}
void DATE::getCount()
{
cout << "There are " << count << " objects now" << endl;
}
void main()
{
DATE DATE1;
DATE DATE2( 1976, 12, 20 );
DATE::getCount();
}

基本知识

C++新类型 bool

C语言:没有bool类型,c 程序员常用预定义定义TRUE、FALSE。C99 定义了 bool 类型,并通过 stdbool.h 实现与 C++兼容

C++语言:

  • 定义了三个关键字:bool, true, false
  • 当显式 ((bool)7) 或隐式 (bool b = 7) 转为 bool 类型时
    • 0 值转为 false
    • 非 0 值转为 true
  • 使用 cout 输出

void 形参

C语言:

  • func(); 没有声明形参表示函数的形参不确定
  • 没有参数,则必须显式声明 func(void);

C++语言:

  • func(); 等价于 func(void);
  • 使用 “...” 标点符号表示 可变参数
1
2
3
4
5
//函数声明如下
int printx(const char* fmt, ...);
//能以一个或多个实参调用
printx("hello world");
printx("a = %d b = %d", a, b);

函数重载

在开发中,需要的多个函数功能相似,但参数的数目或类型不同。例如交换整数和浮点数两个函数。

C语言:必须声明两个不同名的函数,否则会标识符重定义。

C++语言:声明同样名称的一组函数,但必须保证形参类型/数量不同:

1
2
void Swap(int& x, int& y);
void Swap(double& x, double& y);

函数重载与auto

函数重载的优点:程序易于理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
void Swap(int& x,int& y);
void Swap(double& x,double& y);
void Swap(int& x,int& y) {
auto temp = x; x = y; y = temp;
}
void Swap(double& x,double& y) {
auto temp = x; x = y; y = temp;
}
int main() {
int i = 2,j = 3;
Swap(i,j);
cout << i << "," << j << endl;
double x = 2.2, y = 3.3;
Swap(x,y);
cout << x << "," << y << endl;
}

两个实现唯一差别是 temp 类型不同

  • 使用 auto 类型,则实现代码完全一样
  • 让编译器去推导决定哪个函数

函数签名

编译器根据 函数名、参数数目、参数类型 生成唯一的内部函数名

例如:Swap 函数。Swap_int_int 和 Swap_float_float 是不同的函数签名,所以不是一个函数。

C++新特性-默认实参

C语言不支持函数默认参数和值

C++语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
//带默认参数的函数声明
void func(int n = 1, float b = 1.2, char c = '@');
//实现
void func(int n, float b, char c){
cout << n << ", "<< b << ", " << c << endl;
}

int main(){
func(); //func(1,1.2,'@')
func(10); //func(10,1.2,'@')
func(20, 9.8); //func(20,9.8,'@')
func(30, 3.5, '#'); //func(30,3.5,'#')
return 0;
}

默认参数只能定义在参数列表的右边

字符串类型-string

C语言字符串:

  • C字符串是 char* 类型,是以 '\0' 字符结束的字符数组

    const char* s = "c plus plus";

  • 在 C++ 中处理 C字符串,请使用 #include<cstring>

C++语言字符串:

  • string 是类
  • 作为区别,使用 cstring 称c字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#include<string>
using namespace std;

int main(){
string s1; //默认构造
string s2 = "c plus plus"; //用 cstring 构造
string s3 = s2; //用同类对象构造
string s4(5, 's');//用 int, char 作为参数构造
cout << s2 << endl;
cout << s4 << endl;
return 0;
}

访问控制

公有(public)成员

公有成员 在客户端可以任意访问。

公有数据成员 不需要通过公有函数成员访问

  • 优点:使用方便
  • 缺点:可能会破坏封装的逻辑一致性,如客户端修改线段长度为负数
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
/* public.hpp */
#pragma once
namespace sysu_cplus{
class Line{
public:
double length;
void setLength(double len);
double getLength( void );
};
}
// 成员函数定义
double sysu_cplus::Line::getLength(void) {
return length ;
}
void sysu_cplus::Line::setLength( double len ) {
length = len;
}
/* 主程序 */
#include<iostream>
#include "public.hpp"
using namespace std;
using namespace sysu_cplus;
// 程序的主函数
int main( ) {
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
// 不使用成员函数设置长度
line.length = 10.0;
// OK: 因为 length 是公有的
cout << "Length of line : " << line.length <<endl;
// Error: 破坏包装的后果 – 语义错误
line.length = -10.0;
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}

私有(private)成员

私有成员变量或函数 在类的外部不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。

默认情况下,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
/* private.hpp */
namespace sysu_cplus {
class Box {
public:
double length;
void setWidth( double wid );
double getWidth(void) const;
double getArea();
private:
double width;
};
}
// 在函数定义中,我们如何区别 namespace名 和 类名?
double sysu_cplus::Box::getWidth(void) const{
return width ;
}
void sysu_cplus::Box::setWidth(double w) {
width = w;
}
/************/
int main( ) {
Box box;
// 不使用成员函数设置长度
box.length = 10.0;
// OK: 因为 length 是公有的
cout << "Length of box : " << box.length <<endl;
// 不使用成员函数设置宽度
// box.width = 10.0;
// Error: 因为 width 是私有的
// 使用成员函数设置宽度
box.setWidth(10.0);
cout << "Width of box : " << box.getWidth() <<endl;
return 0;
}

构造函数

无参构造函数

类的构造函数是类的一种特殊的成员函数,每次创建类的新对象时执行它完成初始化等逻辑。

构造函数的名称与类的名称是完全相同的,并且 不会返回任何类型,也 不会返回 void

如果用户没有自定义构造函数,则编译会自动生成一个 默认构造函数

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
/* constractor.hpp */
#pragma once
#include<iostream>
namespace sysu_cplus {
class Line {
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
}
// 成员函数定义,包括构造函数
sysu_cplus::Line::Line(void) {
std::cout << "Object is being created" << std::endl;
}
void sysu_cplus::Line::setLength( double len ) {
length = len;
}
double sysu_cplus::Line::getLength( void ) {
return length;
}
/****************/
#include <iostream>
#include "constructor.hpp"
using namespace std;
using namespace sysu_cplus;
// 程序的主函数
int main() {
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}

有参构造函数

构造函数也可以带有参数。这样在创建对象时就可以使用参数构造函数。

注意:用户一旦定义了构造函数,编译器就 不再自动添加 默认构造函数。这时调用无参构造会出错。

构造函数也能使用默认实参 CLASS(int a = 0); 。这样可以减少构造函数重载的数量。

拷贝构造函数

语法:CLASS::CLASS(const CLASS& obj);

  • 有一个形参,其类型为 类类型本身

  • 该参数传递方式为按引用传递,避免在函数调用过程中生成形参副本

  • 该形参声明为 const,确保在拷贝构造函数中不修改实参的值

  • 形参类型为该类类型本身且参数传递方式为按引用传递。

  • 用一个已存在的该类对象初始化新创建的对象。

  • 每个类都必须有拷贝构造函数:

    • 用户可根据自己的需要显式定义拷贝构造函数。
    • 若用户未提供,则该类使用由系统提供的缺省拷贝构造函数(可用=default),也可用 =delete 弃置该函数。
    • 缺省拷贝构造函数按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制,完成新对象的初始化。
    • 即逐一调用成员的拷贝构造函数,如果成员是基础类型,则复制值(赋值)。

隐式调用 复制构造函数

FIRST 对象作为函数形参

隐式调用复制构造(1):将一个对象作为实参,以按值调用方式传递给被调函数的形参对象。

1
2
3
4
void fun(CLASS temp){...}
fun(obja);
//obja传递给fun函数,创建形参对象temp时,调用C的拷贝构造函数用对象obja初始化对象temp
//temp生存期结束被撤销时,调用析构函数

用obja来初始化temp。

  • 如果有为C类明确定义拷贝构造函数,将调用这个拷贝构造函数
  • 如果没有为C类定义拷贝构造函数,将调用缺省的拷贝构造函数

SECOND 对象作为值从函数返回

  • 当函数返回一个对象时,系统将自动创建一个临时对象来保存函数的返回值。创建此临时对象时调用拷贝构造函数
  • 当函数调用表达式结束后,撤销该临时对象时,调用析构函数
1
2
3
4
5
6
7
CLASS fun2()
{
CLASS t;
...
return t;
}
b = fun2();

t => 临时对象 => b 拷贝构造函数 赋值运算符

注:但现在的gcc/g++不这么处理,会做一个优化。在C函数里有个t变量,离开func时不撤销这个 t 对象,而是让(自动创建的临时对象)和这个 t 对象关联起来。也就是(自动创建的临时对象)就是这个 t 对象。

复制策略:拷贝构造函数自定义

  1. 对于不含指针成员的类,使用系统提供(编译器合成)的默认拷贝构造函数即可。
  2. 默认(缺省)拷贝构造函数使用 浅复制策略,是简单的值复制,如果类中有指针数据成员,会有重复释放指针所指内存的风险,不能满足 含指针数据成员的类的需要。
  3. 含指针成员的类通常应重写以下内容:
    • 构造函数(以及拷贝构造函数)中分配内存(深复制策略)
    • = 操作重写,完成对象 深复制策略
    • 析构函数中 释放内存

浅拷贝 只复制成员指针的值,而不复制指向的对象实体,导致新旧对象成员指针指向同一块内存

深拷贝 要求成员指针指向的对象也要复制,新对象与原对象的成员指针不会指向同一块内存,修改新对象不会修改原对象

析构函数

类的析构函数是类的一种特殊的成员函数,它会在对象被释放前执行。

析构函数的名称与类的名称是完全相同的,只是在前面加一个波浪号(~)作为前缀,它

  • 不会返回任何值
  • 也不能带有任何参数

析构函数有助于在跳出程序前释放资源(比如关闭文件、释放内存等)。

注意:析构函数不能直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* constractor.hpp */

namespace sysu_cplus {
class Line {
public:
void setLength( double len );
double getLength( void );
Line(); //构造函数
~Line(); //析构函数
private:
double length;
};
}
// 成员函数定义,包括构造函数
sysu_cplus::Line::Line(void) {
std::cout << "Object is being created" << std::endl;
}
sysu_cplus::Line::~Line(void) {
std::cout << "Object is being deleted" << std::endl;
}

注:

  1. 对于大多数创建的对象,对象生命期结束,对象被销毁前会调用析构函数
  2. new 出来的对象在创建时会调用构造函数,如果使用 delete 则会调用析构对象,否则不会自动调用析构函数。
  3. malloc 不会调用 构造函数,free 不会调用 析构函数

this 指针

在C++中,每个对象都能通过 this 指针来访问自己的地址

  • this 指针所有动态成员函数的隐含参数友元函数没有 this 指针,因为友元函数不是类的成员。静态成员函数没 this 指针
  • 因此,在成员函数内部,它可以用来指向调用对象。

this 指针

  • -> 是取成员运算符,和 c 语言一致。
  • static 成员不能使用 this,应使用 类名::成员
  • 当成员函数的参数与成员数据重名时,必须使用 this 访问成员数据。

如果设计的类没有构造函数,C++编译器会自动为该类型建立一个缺省构造函数。该构造函数没有任何形参,且函数体为空。

• 应该养成编写构造函数的习惯。

对象成员初始化

类的非静态数据成员可以用下列几种方式之一初始化:

  1. 通过默认成员函数初始化器,它是成员声明中包含的 花括号或等号初始化器。(C++11)
  2. 在构造函数的 成员初始化器列表 中。(C++11)
  3. 构造函数体内进行赋值操作。(注意:不能构造成员,除非你特别熟悉 new 的各种用法)

对象初始化分两个阶段:首先按声明顺序初始化成员、然后执行构造函数函数体

为什么需要初始化容器列表和默认成员初始化器?

  • 对于复杂对象成员,必须先构造才能在构造函数的函数体中赋值 这会导致构造函数必须构造一个 string 中间变量
  • 对于 引用类型的数据成员 和 const数据成员 需要初始化
  • 对象初始化需要参数

为了提升初始化性能,C++引入了 默认初始化器 和 初始化列表

成员初始化器(C++11)

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化器列表,初始化器列表以冒号开头,后跟一系列以 逗号 分隔的成员初始化器。

1
2
3
4
5
6
7
8
9
class DATE{
public:
DATE( int initYear, int initMonth, int initDay ):year(initYear), month(initMonth), day(initDay){}
DATE();
private:
int month;
int day;
int year;
};

必须使用成员初始化器必须使用的情况:

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化器列表里面
  2. 引用成员,引用必须在定义的时候初始化,并且不能重新赋值,所以也要使用初始化器
  3. 如果某个成员的类类型没有无参数的构造函数,则要使用初始化器直接调用有参数的构造函数初始化

数据成员初始化的顺序:是按照它们在 类中声明的顺序 进行初始化的,而不是按照它们在初始化器列表出现的顺序初始化的。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
class foo {//foo1.cpp
public:
int i ;int j ;
foo(int x):i(x), j(i){} // ok, 先初始化i,后初始化j
};
/*------------------------------------------------*/
class foo //foo2.cpp
{
public:
int i ;int j ;
foo(int x):j(x), i(j){} // false, i值未定义
};

这里i的值是未定义的因为虽然j在初始化器列表里面出现在i前面,但是i先于j定义,所以先初始化i,而i由j初始化,此时j尚未初始化,所以导致i的值未定义。

最好是 按照成员声明的顺序进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct S{
int n; //非静态数据成员
int& r = n; //引用类型的非静态数据成员;
int a[2] = {1, 2}; //带默认初始化器的非静态数据成员
std::string s{'H', 'C'};
struct NestedS {
std::string s;
NestedS(std::string s = "hello"):s(s){} //构造函数
}d5; //具有嵌套类型的非静态数据成员

const char bit: 2; //2位位域,const 初始化
S(): n(7), bit(3){} // ":n(7),bit(3)" 是初始化器列表; "{}" 是函数体
S(int x):n(x), bit(3){}
}

对象的内存布局

对象不存储指向成员函数的指针,类的成员函数存储在代码段,是所有类对象的共用空间

构造对象时为数据成员提供存储空间。

对象方法的 静态联编

指在编译阶段,就能直接使用代码f段函数地址调用动态对象的方法。该方法仅需要向非静态成员函数传送 this 指针,即可用静态函数调用实现动态调用的效果。

优势:

  1. 对象布局与C结构内存布局一致,使得内存中对象便于与其他语言程序库兼容
  2. 高效率, 高性能

运算符重载

定义:运算符重载 是指 重载具有特殊函数名(即以 运算符为函数名)的函数,也具有返回值类型,函数名和参数列表。函数名是由 关键字operator 和其后要 重载的运算符符号 构成的。

运算符重载的两个方法:

  • 类成员函数 运算符重载
    • return_type class_name::operator op(operand2){ }
    • 重载二元运算符时,成员运算符函数只需显式传递一个参数(即二元运算符的由操作数),而左操作数则式该类对象本身,通过 this 指针隐式传递。
    • 重载一元运算符时,成员运算符函数没有参数,操作数时该类对象本身,通过 this 指针隐式调用。
  • 友元函数 运算符重载
    • return_type operator op(operand1, operand2)

可以重载的运算符

能重载的运算符

大多数 运算符都可以通过成员函数 或 非成员函数进行重载。

但下面的运算符只能通过成员函数重载。

  • =:赋值运算符
  • ():函数调用运算符
  • []:下标运算符
  • ->:通过指针访问类成员的运算符

不可以重载的运算符

  • :: (作用域解析)
  • .(成员访问)
  • .*(通过成员指针的成员访问)
  • ?: (三元条件)
  • sizeof运算符 ,还有除 new, delete 外的关键字运算符,如alignoftypedef 等等
  • # (预处理符号)

其他限制

  • 不能创建新运算符
  • 运算符 &&|| 的重载失去短路求值
  • 重载的运算符 -> 必须要么返回 裸指针,要么(按引用或值)返回 同样重载了的运算符 -> 的对象
  • 不可能更改运算符的优先级、结合方向或操作数的数量

注意

  • 运算符作为非成员函数重载的一个常见原因是,左操作数不是该类的对象,而是其他类型的数据,例如 int、double 等。在这种情况下,运算符必须作为非成员函数重载,以便能够接受该类型的数据作为左操作数。另一个原因是,如果运算符需要返回一个引用,那么它必须作为非成员函数重载,因为成员函数不能返回引用类型的值。
  • 作为非静态成员函数重载的运算符,其第一个参数是隐式的,表示该运算符作用于哪个对象。 对于二元运算符,可以有两个参数,其中第一个参数是隐式的,第二个参数是显式的,表示运算符的右操作数。对于一元运算符,只有一个参数,即隐式的对象指针。
  • 运算符重载的使用方式与运算符函数是作为成员函数实现的还是作为非成员函数实现的无关。
  • y+=z 等价于 y.operator+=(z)

注意事项

自增运算符

重载自增运算符分为两种情况:

  • 重载 前缀自增运算符,直接重载

    return_type class_name::operator++() {}

  • 重载 后缀自增运算符,该函数有一个 int 类型的虚拟形参,它告诉编译器递增运算符以 后缀模式 重载: return_type class_name::operator ++(int) {}

返回 值 还是 引用?

  • 内建运算符的 前置版本 返回 引用,而 后置版本 返回 值。
  • 用户自定义的重载中,能以任何形式为返回类型。
  • 重载前置 ++x 的方法
    • 保持语义:x 的值加1。作为表达式,返回加1之后的值
    • 返回 对象引用 的好处:
      • 返回当前对象 能够实现 “返回加 1 之后的值” 这个语义
      • 返回引用 能够减少构造新对象的成本
      • 运算结果 可以作为 左值
  • 重载后置 x++ 的方法
    • 形式: 由一个形参
    • 保持语义:x 的值加 1。作为表达式,返回加 1 之前的值
    • 不能返回 对象引用的原因
      • 由于当前对象保持了加 1 之后的值,因此不能作为返回值
      • 只能返回新建的对象(加 1 之前的值)
    • 不能重载为 const 方法,因为要加 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Integer{
int x;
public:
Integer(int x = 0): x(x){}
Integer* operator++()
{
cerr << "prefix is invoked" << endl;
++x;
return * this;
}

Integer operator ++(int)
{
cerr << "suffix is invoked" << endl;
return Integer(x++);
}

void print()
{
cout << x << endl;
}
};

赋值运算符

重载 赋值运算符= 的方法

  • 保持语义:将运算符=右侧表达式的值赋给左侧变量
  • 参数:
    • 表示运算符=右侧表达式的值
    • 使用引用是为了效率
    • 使用 const 是为了安全(符合赋值的常规语义)
  • 返回值:使用引用返回是为了效率

在用户自定义类中,如果有指针类型的数据成员,则最好 自定义拷贝构造函数 和 重载赋值运算符,实现深拷贝,避免使用 默认的拷贝构造函数 和 默认的缺省的赋值运算符(均为浅拷贝,可能导致重复释放已经释放过的指针指向的空间)。

移位运算符

  • 第一操作数 不是 *this,只能类外定义
  • 普通函数重载 <<>>
    • 保持语义
      • << 是输出,支持级联输出,如 cout << "x="<< x << endl;
      • >> 是输入,支持级联输入,如 cin >> x >> y;
    • 参数
      • 第 1(左)操作数:流对象
      • 第 2 (右)操作数:自定义类的对象
      • 使用引用是为了效率
    • 返回值:返回流对象(即第一个操作数)的引用,以支持级联 IO

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Integer{
int x;
};

istream& operator(istream& is, Integer* Int)
{
is >> Int.x;
return is;
}

ostream& operator<<(ostream& os, const Integer* Int)
{
os << Int.x;
return os;
}

int main()
{
Integer x;
cin >> x;
cout << x << endl;
return 0;
}

函数对象

如果一个类定义了 operator() 运算符函数,那么使用该类的对象可以调用这个运算符函数,其调用形式如同 普通函数调用 一般,因此取名叫 函数对象

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class cmp{
public:
bool operator()(const int& a, const int& b){
return a<b;
}
}

int main()
{
cmp f;
cout << f(1, 2) << endl;
cout << f(2, 1) << endl;
return 0;
}

输出:

1
2
1
0

因为 函数对象 实际上也是一个对象,所以也可以拥有自己的成员变量,从而也可以表示 谓词判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GreaterThan{
int baseline;
public:
GreaterTan(int x): baseline(x){}
bool operator()(const int& x)
{
return x > baseline;
}
}

int main()
{
GreaterThan G1(10), G2(20);
cout << G1(15) << endl;
cout << G2(15) << endl;
return 0;
}

输出结果:

1
2
1
0

所以,函数对象相比普通的函数有一个非常重要的用途,即作为 谓词函数(Predicate)。谓词函数通常用来对传进来的参数进行判断,并返回 布尔值。标准库中有大量定义了多个重载版本的函数,其中有的函数要求用户提供 谓词函数,比如:find_if,remove_if,等等。

例:

  • 第一种:函数对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class GreaterThan{
    int baseline;
    public:
    GreaterThan(int x): baseline(x){}
    bool operator()(const int& x){
    return x>baseline;
    }
    }

    int main()
    {
    vector<int> a = {5, 10, 15, 20, 25};
    //find_if return a iterator
    cout << *find_if(a.begin(), a.end(), GreaterThan(10)) << endl;
    cout << *find_if(a.begin(), a.end(), GreaterThan(20)) << endl;
    }
  • 第二种:普通 bool 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    bool GreaterThan10(const int& x){
    return x>10;
    }
    bool GreaterThan20(const int& x){
    return x>20;
    }

    int main()
    {
    vector<int> a = {5, 10, 15, 20, 25};
    //find_if return a iterator
    cout << *find_if(a.begin(), a.end(), GreaterThan10) << endl;
    cout << *find_if(a.begin(), a.end(), GreaterThan20) << endl;
    }

友元函数

类的成员函数也是函数的一种,所以其他类的成员函数也可以是友元函数!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Integer;
struct Cmp{
bool operator()(const Integer& a, const Integer& b)
};

class Integer{
int x;
public:
Integer(int x = 0): x(x){}
friend bool Cmp::operator()(const Integer& a, const Integer& b);
};

bool Cmp::operator()(const Integer& a, const Integer& b)
{
return a.x < b.x;
}

但有些时候,其他类的成员函数可能会很多,一个一个的声明为友元函数会比较麻烦。 所以我们就可以直接声明友元类:

  • 一个类 A 可以将另一个类 B 声明为自己的友元,那么类 B 的所有成员函数就都可以访问类 A 对象的私有成员
  • 形式:friend class B; (在类 A 的内部)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Integer;
struct Cmp{
bool operator()(const Integer& a, const Integer& b)
};

class Integer{
int x;
public:
Integer(int x = 0): x(x){}
friend Cmp;
}

bool Cmp::operator()(const Integer& a, const Integer& b)
{
return a.x < b.x;
}

explicit 说明符

  1. 指定构造函数或转换函数 (C++11 起)或推导指引 (C++17 起)为显式,即它不能用于隐式转换和复制初始化。
  2. explicit 说明符可以与常量表达式一同使用。函数在且只会在该常量表达式求值为 true 时是显式的。(C++20 起) explicit 说明符只能在 类定义之内的构造函数 或 转换函数 (C++11 起)的 声明说明符序列 中出现。

声明时不带函数说明符 explicit 的拥有单个无默认值形参的 (C++11 前)构造函数被称作转换构造函数。

构造函数(除了复制或移动)和用户定义转换函数都可以是函数模板;explicit 的含义不变。

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
struct A
{
A(int) { } // 转换构造函数
A(int, int) { } // 转换构造函数(C++11)
operator bool() const { return true; }
};

struct B
{
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};

int main()
{
A a1 = 1; // OK:复制初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A::operator bool()
bool na1 = a1; // OK:复制初始化选择 A::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化

// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
B b5 = (B)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}

继承和派生

Q:在什么情况下使用?

A:在 C++ 中,当定义一个新的类 B 时,如果发现类 B 拥有某个已写好的类 A 的全部特点,此外还有类 A 没有的特点。

此时不必从头重写类B,而是可以

  • 把类 A 作为一个 “基类”(也称 “父类”)
  • 把类 B 写为基类 A 的一个 “派生类” (也称子类)
  • 这样,就可以说从类 A “派生”出了类 B,也可以说类 B “继承”了类 A。

效果:派生类是通过对基类进行扩充和修改得到的。基类的所有成员自动成为派生类的成员。

Q:为什么要提出继承与派生的概念?

A

  • 提供派生类的概念及其相关语言机制是为了表达 层次关系,即表达 类之间的共性。
    • 派生类是通过对基类进行扩充和修改得到的。
    • 基类的所有成员自动成为派生类的成员。
  • 它是通常所说的 面向对象编程 的基础。
    • 如果只有一个类的概念,软件的可重用性、演化和相关的概念表示存在严重的不灵活问题。
    • 继承机制为软件可重用性、IS-A 概念表示和易于修改提供了解决方案。
  • 继承提供了一种通过修改(演化)一个或多个现有类来构造新类的方法。

继承与派生的优势

image-20230504102514795

术语

4个表示两个类继承关系的术语

image-20230504101943996

image-20230504102105604

C++中继承关系图解

image-20230504102614055

语法

C++继承关系语法基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal{
public:
void eat(){
isEaten = true;
cout << "I have eaten" << endl;
}
protected:
Animal(){ cout << "Animal is constructing"<< endl; }
bool isEaten = false;
};

class Dog: public Animal{
public:
Dog(): Animal(){cout << "Dog is constructing"<< endl; }
void bark()
{
if(isEaten)
cout << "I am barking, wang wang wang" << endl;
else
cout << "I am starving, wow" << endl;
}
};

继承语法

  • 单重继承的定义形式:

    1
    2
    3
    4
    class 派生类名: 继承访问控制 基类类名{
    成员访问控制:
    成员声明列表;
    };

    继承访问控制和成员访问控制均由保留public、protected、private来定义,缺省均为private。

成员访问控制

  • 公有继承(public):
    • 在public后声明的成员称为公有成员
    • 公有成员用于描述一个类与外部世界的接口
    • 类的外部(程序的其它部分的代码)可以访问公有成员
  • 保护继承(protected):
    • 受保护成员具有private与public的双重角色:
      • 对派生类的成员函数而言,它为public
      • 而对类的外部而言,它为private
    • protected成员只能由本类及其后代类的成员函数访问。
  • 私有继承(private):
    • 在private后声明的成员称为私有成员
    • 私有成员只能通过本类的成员函数来访问

继承成员的访问控制

影响 继承成员(派生类从基类中继承而来的成员)访问控制方式的两个因素:

  • 定义派生类时指定的 继承访问控制
  • 该成员在 基类 中所具有的成员访问控制

image-20230504115449591

  • 无论采用什么继承方式,基类的私有成员在派生类中都是不可访问的。
  • “私有”和“不可访问”有区别:私有成员可以由派生类本身访问,不可访问成员即使是派生类本身也不能访问。
  • 大多数情况下均使用 public 继承

访问控制举例

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
class BASE
{
public:
BASE();
void get_ij();
protected:
int i, j;
private:
int x_temp;
};

//公有派生:在Y1类中,i、j是受保护成员
class Y1: public BASE
{
public:
void increment(); //get_ij()是公有成员,x_temp不可访问
private:
float nmember;
};

BASE::BASE()
{ i=0; j=0; x_temp=0; }
void BASE:: get_ij()
{
cout << i << " " << j << endl;
}
void Y1::increment()
{
i++; j++;
}

int main() //程序Access
{
BASE obj1;
Y1 obj2;
obj2.increment();
obj2.get_ij();
obj1.get_ij();
}

保护派生:在Y2类中,i、j是受保护成员。get_ij()变成受保护成员,x_temp不可访问

class Y2:protected BASE{ … };

私有派生:在Y3类中,i、j、 get_ij()都变成私有成员,x_temp不可访问

class Y3:private BASE{ … };

  • 在大多数情况下,使用public的继承方式;private和protected是很少使用的。 ✓ 微软的MFC:全部使用public的继承方式 ✓ AT&T的iostream库:95%以上使用的是public的继承方式

派生类对象的存储

派生类的对象不仅存放了在派生类中定义的非静态数据成员,而且也存放了从基类中继承下来的非静态数据成员

构造

继承时的构造函数

  • 基类的构造函数 不被继承,派生类中需要定义自己的构造函数。

    • 派生类的构造函数中只需要对本类中新增成员进行初始化即可。
  • 对继承来的基类成员的初始化:调用基类的构造函数

    ① 显示在 初始化器列表 中调用(注:不能在构造函数内调用!)

     特别是需要使用基类的有参构造函数

    ② 隐式调用:

    • 编译时在派生类构造函数初始化器中自动生成对基类默认构造函数的调用。
    • 如果基类没有默认构造(包括 =delete),则编译错误
  • (创建派生类对象时)构造函数的调用次序 ① 首先调用其基类的构造函数(调用顺序按照基类被继承时的声明顺序(从左向右))。

    ② 然后调用本类对象成员的构造函数(调用顺序按照对象成员在类中的声明顺序)。

    ③ 最后调用本类的构造函数。

继承时的析构函数

  • 撤销派生类对象时析构函数的调用次序与构造函数的调用次序相反

    ① 首先调用本类的析构函数

    ② 然后调用本类对象成员的析构函数

    ③ 最后调用其基类的析构函数

举例

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
//Demo.h
class C {
public:
C( ); //构造函数
~C( ); //析构函数
};
class BASE {
public:
BASE( ); // 构造函数
~BASE( ); // 析构函数
};


//Demo.cpp
#include “Demo.h”
C::C( ) //构造函数
{
cout << "Constructing C object.\n";
}
C:: ~C( ) //析构函数
{
cout << "Destructing C object.\n";
}
BASE::BASE( ) // 构造函数
{ cout << "Constructing BASE object.\n"; }
BASE:: ~BASE( ) // 析构函数
{ cout << "Destructing BASE object.\n";}


class DERIVED: public BASE { // Derived.h
public:
DERIVED() // 构造函数
~DERIVED() // 析构函数
private:
C mOBJ;
};


#include “Derived.h” // Derived.cpp
DERIVED::DERIVED() // 构造函数
{ cout << "Constructing derived object.\n"; }
DERIVED:: ~DERIVED() // 析构函数
{ cout << "Destructing derived object.\n"; }

// Client.cpp
#include “Derived.h”
int main()
{
DERIVED obj; // 声明一个派生类的对象
// 什么也不做,仅完成对象obj的构造与析构
return 0;
}

类中不可继承的成员

类中哪些成员不可继承?

  • 私有成员(注:只是不可访问)
  • 构造函数和析构函数

例:

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

class MyString: public std::string()
{
public:
MyString(const char* s): std::string(s){}
};

int main()
{
MyString str1("继承 string(const char*)");
str1 = "继承 string::operator=(...)";
std::cout << str1 << std::endl;
MyString str2 = "hello";
std::cout << str2+str1 << std::endl;

return 0;
}

向基类构造函数传递实参

  • 基类构造函数带参数,则定义派生类构造函数时,仅能 通过 初始化列表显式调用 基类构造函数,并向基类构造函数传递实参。

  • 带初始化列表的派生类构造函数的一般形式如下:

    1
    2
    3
    4
    派生类名(形参表):基类名(实参表)
    {
    派生类新成员初始化赋值语句;
    };

示例

Time 类

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
//time.h

class Time
{
public:
void Set(int hours, int minutes, int seconds);
void Increment();
void Write() const;
Time(int initHrs, int initMins, int initSecs); // constructor
Time(); // default constructor
};

//exttime.h

#include"time.h"
enum ZoneType{EST, CST, MST, PST, EDT, CDT, MDT, PDT };

class ExtTime: public Time
{
public:
ExtTime(int initHrs, int initMins, int initSecs, ZoneType initZone);
ExtTime();
void Set(int hours, int minutes, int seconds, ZoneType timeZone);
void Write() const;

private:
ZoneType zone;
};

//exttime.cpp

ExtTime::ExtTime(int initHrs, int initMins, int initSecs, ZoneType initZone): Time(initHrs, initMins, initSecs)
{
zone = initZone;
}
ExtTime::ExtTime()
{
zone = EST;
}

void ExtTime::Set( int hours, int minutes, int seconds,ZoneType timeZone )
{
Time::Set(hours, minutes, seconds);
zone = timeZone;
}

void ExtTime::Write() const
{
static string zoneString[8] =
{ "EST", "CST", "MST", "PST", "EDT", "CDT", "MDT", "PDT“ };
Time::Write();
cout << ' ' << zoneString[zone];
}

实现带初始化列表的派生类构造函数的注意事项

  1. 将参数传递给基类构造函数。
  2. 基类构造函数在派生类构造函数之前调用。
  3. 这里更建议将 zone(initZone)加入初始化器列表中,更加有C++风格

派生与成员函数

  • 重载(overload)

    • 具有相同作用域(即同一个类定义中)
    • 函数名字相同
    • 参数类型(包括 const 指针或引用),顺序 或 数目不同
  • 覆盖(override)

    • 修改基类函数定义
  • 隐藏(overwrite)

    屏蔽 基类的函数定义

    ① 派生类的函数 与 基类的函数 同名,但是参数列表有所差异。

    ② 派生类的函数 与 基类的函数 同名,参数列表也相同,但是基类函数没有 virtual 关键字。

  • 继承

    没有被覆盖或隐藏的基类函数,包括基类中重载的函数

重载和隐藏的区别

例如:Time::Set(int, int, int)ExtTime::Set(int, int, int, ZoneType)

  • 如果 Set 在一个类中定义,则是 重载(Set 的 函数标签不一样)
  • 如果同名函数出现在 Base 和 Derived 中,且满足特征①或②,则属于 隐藏
    • ExtTime 案例 Set 满足特征 ①
    • ExtTime 案例 Write 满足特征 ②

在派生类中隐藏与显式调用基类中的成员函数

  • 在派生类中调用被隐藏的函数成员
    • 方法:Base::HidenFun(...)this->Base::HidenFun(...)

隐藏的应用

  • 利用隐藏,在派生函数中修改成员函数的功能
  • 利用隐藏,赋予派生类成员函数新的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//time.cpp
void Time::Set( int hours, int minutes, int seconds )
{
hrs = hours;
mins = minutes;
secs = seconds;
}

//Exttime.cpp
void ExtTime::Set( int hours, int minutes, int seconds,ZoneType timeZone )
{
Time::Set(hours, minutes, seconds);
zone = timeZone;
}

改变继承访问控制方式

恢复访问控制方式

  • 基类中的 public 或 protected 成员,因使用 protected 或 private 继承访问控制而导致在派生类中的访问方式发生改变,可以使用 “访问声明” 恢复为原来的访问控制方式

  • 访问声明的形式

    using 基类名:: 成员名 (放在适当的成员访问控制后)

  • 使用情景

    在派生类中希望大多数继承成员为 protected 或 private,只有少数希望保持为基类原来的访问控制方式。

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

class BASE {
public:
void set_i(int x)
{
i = x;
}
int get_i()
{
return i;
}
protected:
int i;
};

class DERIVED : private BASE {
public:
using BASE::set_i; // 访问声明
using BASE::i;
void set_j(int x)
{
j = x;
}
int get_ij()
{
return i + j;
}
protected:
int j;
};

int main()
{
DERIVED obj; // 声明一个派生类的对象

obj.set_i(5); // set_i()已从私有的转为public
obj.set_j(7); // set_j()本来就是公有的
cout << obj.get_ij() << "\n"; // get_ij()本来就是公有的

return 0;
}

屏蔽基类成员

目的:使得客户代码通过派生类对象不能访问继承成员。

方法:

  • 使用继承访问控制 protected 和 private(真正屏蔽)
  • 在派生类中成员访问控制 protected 或 private 之后,使用 “using 基类名::成员名”(非真正屏蔽,仍可通过使用“基类名::成员名” 访问)

用于继承对象的重命名

目的:解决名字冲突,在派生类中选择更合适的术语命名继承成员

方法:①在派生类中定义新的函数,该函数调用旧函数;屏蔽旧函数。

②在派生类中定义新的函数,该函数的函数体与旧函数相同

使用基类构造函数

目的:使得派生类对象直接使用基类的构造函数

方法:在派生类中使用 "using 基类名::基类名"

类型兼容性

  • 赋值运算的类型兼容性

    • 可以将后代类的对象赋值给祖先类对象,反之不可以。
    1
    2
    3
    4
    BASE obj1;
    Y1 obj2;
    obj1 = obj2; //可以把obj2中基类的部分内容赋给obj1
    //obj2 = obj1; //错误

    原因:每个派生类对象包含一个基类部分,这意味着可以将派生类对象当作基类对象使用。

  • 指向基类对象的指针 也可指向 公有派生类对象

  • 只有公有派生类才能兼容基类类型(上述规则只适用于公有派生)

类型兼容性例子

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
//B.h
class Base{
public:
void display()
};
class D1: public Base(){
public:
void display();
};
class D2: public D1{
public:
void display();
};

//B.cpp
#include "B.h"
void Base::display(){
cout << "Base::display" << endl;
}
void D1::display(){
cout << "D1::display" << endl;
}
void D2:: display(){
cout<<"D2::display()"<<endl;
}

//main.cpp
void fun(Base &ptr)
{
ptr->display();
}

int main()
{
Base b; //声明Base类对象
D1 d1; //声明D1类对象
D2 d2; //声明D2类对象
Base *p;//声明Base类指针
p=&b; //Base类指针指向Base类对象
fun(p);
fun(b);
p=&d1; //Base类指针指向D1类对象
fun(p);
fun(d1);
p=&d2; //Base类指针指向D2类对象(间接派生类对象)
fun(p);
fun(d2);
}

运行结果

1
2
3
Base::display()
Base::display()
Base::display()

对象的类型转换

C++向上转型和向下转型

  1. 隐式转型(向上转型,即将派生类对象赋值给基类)

    C++允许向上转型,即将 派生类对象赋值给基类的对象 是可以的。其只不过是将派生类中基类的部分直接赋值给基类的对象,这称为向上转型(这里的“上”是指基类),例如:

    1
    2
    3
    4
    5
    6
    7
    8
    class Base{};
    class Derived: public Base{};

    Base* Bptr;
    Derived* Dptr;

    Bptr = Dptr; //编译正确,允许向上类型转型
    Dptr = Bptr; //编译错误,C++不允许隐式的向下转型
  2. 向下转型

    正如上面所述,类层次间的向下转型是不能通过隐式转换完成的。此时要想达到这种转换,可以借助 static_cast 或者 dynamic_cast

    • static_cast

      例如:

      1
      2
      3
      4
      5
      6
      class Base{};
      class Derived: public{};

      Base* B;
      Derived* D;
      D = static_cast<Drived*>(B); //正确,可以使用static_cast向下转型

      注意:static_cast 的使用,仅当类型之间可隐式转化时才是合法的。static_cast 可以完成类层次之间的向下转型。

    • dynamic_cast (更安全)

      dynamic_cast 涉及运行时的类型检查。如果向下转型是安全的(也就是说,如果基类指针或者引用确实指向一个派生类的对象),这个运算符会传回转型过的指针。如果 downcast 不安全(即基类指针或者引用没有指向一个派生类的对象),这个运算符会传回空指针。

      注意:要使用 dynamic_cast 类中必须定义虚函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Base{
      public
      virtual void fun(){}
      };
      class Drived : public Base{
      public:
      int i;
      };
      Base *Bptr = new Drived();//语句0
      Derived *Dptr1 = static_cast<Derived *>(Bptr); //语句1;
      Derived *Dptr2 = dynamic_cast<Derived *>(Bptr); //语句2;

      此时语句1和语句2都是安全的,因为此时Bptr确实是指向的派生类,虽然其类型被声明为Base*,但是其实际指向的内容确确实实是Drived对象,所以语句1和2都是安全的,Dptr1和Dptr2可以尽情访问Drived类中的成员,绝对不会出问题。

      但是此时如果将语句0改为这样:

      1
      Base *Bptr = new Base();

      那语句1就不安全了,例如访问Drived类的成员变量i的值时,将得到一个垃圾值。(延后了错误的发现) 语句2使得Dptr2得到的是一个空指针,对空指针进行操作,将发生异常,从而能够尽早的发现错误,这也是为什么说dynamic_cast更安全的原因。

  3. 多继承时的向下转型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base1{
    virtual void f1(){}
    };
    class Base2{
    virtual void f2(){}
    };
    class Derived: public Base1, public Base2{
    void f1(){}
    void f2(){}
    };
    Base1 *pD = new Derived;
    Derived *pD1 = dynamic_cast<Derived*>(pD); //正确,原因和前面类似
    Derived *pD2 = static_cast<Derived*>(pD); //正确,原因和前面类似
    Base2 *pB1 = dynamic_cast<Base2*>(pD); //语句1
    Base2 *pB2 = static_cast<Base2*>(pD); //语句2

    此时的语句1,将pD的类型转化为Base2*,即:使得pB1指向Drived对象的Base2子对象,为什么能达到这种转化?因为dynamic_cast是运行时才决定真正的类型,在运行时发现虽然此时pD的类型是Base1*,但是实际指向的是Derived类型的对象,那么就可以通过调整指针,来达到pB1指向Derived 对象的Base2子对象的目的;

    但是语句2就不行了,其使用的是static_cast,它不涉及运行时的类型检查,对于它来讲,pD的类型是Base1*,Base1和Base2没有任何关系,那就会出现编译错误了。error: invalid static_cast from type ‘Base1*’ to type ‘Base2*’

    总结:对于多种继承,如果pD真的是指向Derived,使用static_cast和dynamic_cast都可以转化为Derived,但是如果要转化为Base1的兄弟类Base2,必须使用dynamic_cast,使用static_cast不能通过编译。

    ps:因为Derived和Base1和Base2*之间存在隐式转化,可以将语句2修改为:

    1
    Base2 *pB2 = static_cast<Base2*>(static_cast<Derived*>(pD));

    这样就可以完成转换。

  4. const_cast

    语法:const_cast<type_id> (expression)

    该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。

    一、常量指针被转化成非常量的指针,并且仍然指向原来的对象;

    (去除指向常变量指针的常变量性)

    二、常量引用被转换成非常量的引用,并且仍然指向原来的对象;

    三、const_cast一般用于修改底指针。如const char *p形式。

多继承

派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。

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

1
2
3
class D: public A, private B, protected C{
//类D新增加的成员
}

多继承下的构造函数

1
2
3
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
//其他操作
}

先调用 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
#include <iostream>
using namespace std;
//基类
class BaseA{
public:
BaseA(int a, int b);
~BaseA();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
cout<<"BaseA destructor"<<endl;
}
//基类
class BaseB{
public:
BaseB(int c, int d);
~BaseB();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
cout<<"BaseB destructor"<<endl;
}
//派生类
class Derived: public BaseA, public BaseB{
public:
Derived(int a, int b, int c, int d, int e);
~Derived();
public:
void show();
private:
int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
cout<<"Derived destructor"<<endl;
}
void Derived::show(){
cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}
int main(){
Derived obj(1, 2, 3, 4, 5);
obj.show();
return 0;
}

虚基类

  • 继承基类时,在继承访问控制前添加保留字“virtual”。 那么这个基类就是一个虚拟基类。
    • 虚拟基类用于共享继承。
    • 普通基类与虚基类之间的区别只有在派生类重复继承了某一基类时才表现出来。
  • 创建后代类对象时,virtual 关键字保证了虚基类的唯一副本只被初始化一次
    • 若派生类有一个虚基类作为祖先类,则在派生类构造函数中需要列出对虚基类构造函数的调用(否则,调用虚基类的默认构造函数),且对虚基类构造函数的调用总是先于普通基类的构造函数
    • 创建派生类对象时构造函数的调用次序: ① 最先调用虚基类的构造函数; ② 其次调用普通基类的构造函数,多个基类则按派生类声明时列出的次序、从左到右调用,而不是初始化列表中的次序; ③ 再调用对象成员的构造函数,按类声明中对象成员出现的次序调用,而不是初始化列表中的次序 ④ 最后执行派生类的构造函数。
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
class Base {public: int i;};
class Base1: virtual public Base {
public:
int j;
};
class Base2: virtual public Base {
public:
int k;
};
class Derived: public Base1, public Base2 {
public:
int sum;
};
int main() {
Derived obj; // 声明一个派生类对象
obj.i = 3; // 正确:从Base继承的i在Derived中只有一份
obj.j = 5; // 正确:使用从Base1继承的j
obj.k = 7; // 正确:使用从Base2继承的k
return 0;
}

class derivedA : public baseB, virtual public baseA
{
public:
derivedA()
{
cout << endl << "This is derivedA class." << endl;
}
};
class derivedB : public baseB, virtual public baseA
{
public:
derivedB()
{
cout << endl << "This is derivedB class." << endl;
}
};
class Derived : public derivedA, virtual public derivedB
{
public:
Derived()
{
cout << endl << "This is Derived class." << endl;
}
};
int main()
{
Derived obj;
return 0;
}

运行结果

1
2
3
4
5
6
This is baseA class.
This is baseB class.
This is derivedB class.
This is baseB class.
This is derivedA class.
This is Derived class.

构造顺序: 1.先基类后成员 2.先虚后实 3.先左后右 析构顺序: 与构造顺序相反

多态

虚函数

虚函数是一个类的成员函数,前面有关键字 virtual

作用:

  1. 提醒在 公有继承层次中的一个或多个派生类中要对虚函数进行重定义。
  2. 并且当使用基类指针(或引用)调用派生类的对象的虚函数时,将调用该对象的虚函数的重定义版本。

覆盖(override) 与 隐藏(overwrite)

  • 覆盖(override)

    • 形式:派生类定义的函数与从(基类或非直接基类中)继承的虚函数有同样的签名,即函数名,参数类型,顺序和数量都必须相同。
    • 效果:与同名的继承虚函数是语义相关的,将修改(即重新定义)继承虚函数的实现语义
  • 隐藏(overwrite)

    • 形式 1:派生类定义的函数与继承成员函数同名,但是参数列表有所差异。
    • 形式 2:派生类定义的函数与继承成员函数同名,参数列表也相同,但是基类函数没有 virtual 关键字。
    • 效果:与同名的继承成员是语义无关的, 将屏蔽继承成员(即重新声明这个函数名)
  • 使用效果的场景:

    • 对象指针(引用)被向上转型到基类指针(引用)。即通过指向派生类对象的基类指针(引用)调用同名函数。
      • 覆盖:调用派生类对象的虚函数版本。
      • 隐藏:调用基类的函数。
    • 派生类对象赋值到基类对象,即通过基类对象调用同名函数
      • 按基类行为调用该函数

多态的概念

程序语言中,哪些标识符有哪些会满足多态定义?

  • 函数重载(Function Overload)

  • 方法覆盖(Method Override)

    有虚函数基类的 指针或引用,它可指代派生类的对象

  • 泛型(Generics)/模板(Template),即“参数化类型”

    模板名,例如 vector 可泛化指代各种类型数据的数组

多态标识符必须指派具体的函数或方法以实现规定的语义。

静态绑定:如果在运行前由编译完成这个指派,称为静态绑定

动态绑定:如果在运行期间完成这个指派,称为动态绑定

静态类型:对程序进行编译时分析所得到的表达式的类型被称为表达式的静态类型。程序执行时静态类型不会更改。

动态类型:若某个泛左值表达式(如指针、引用)指代某个多态对象,则其最终派生对象的类型被称为其动态类型。

1
2
3
4
5
6
struct B { virtual ~B() {} }; // 多态类型:至少包含一个虚函数
struct D: B {}; // 多态类型
D d; // 最终派生对象
B* ptr = &d;
// (*ptr) 的静态类型为 B 。什么条件下 *Ptr 是静态类型?final 类
// (*ptr) 的动态类型为 D

静态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Fruit{
public:
void say() {
printf("I'm a fruit!\n");
}
};
class Apple : public Fruit{
};
class Banana : public Fruit{
};
class Cherry : public Fruit{
};
int main(){
Apple a;
Banana b;
Cherry c;
a.say();
b.say();
c.say();
}

从继承的特性可知, Apple、Banana、Cherry 这3个类中即使没有实现“say”函数,会自动继承Fruit类的函数。

在 main 函数中, 对a.say() 编译会延Apple继承树向上,找到最近的say成员函数定义,并翻译为call Fruit::say(a),即 静态联编

所以main函数中三次调用都会输出“I‘m a fruit!”。显然,我们想要它们输出自己的水果种类,那就期望重写(Override or Overwrite)“say”函数。

重写一个相同函数名、相同参数的函数,会覆盖或隐藏之前继承而来的函数。那么再次调用Apple对象的“say”函数时,就不会输出“I‘m a fruit!”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Fruit{
public:
void say() {
printf("I'm a fruit!\n");
}
};
class Apple : public Fruit{
public:
void say() {
printf("I'm an apple!\n");
}
};
class Banana : public Fruit{
public:
void say() {
printf("I'm a banana!\n");
}
};

这里 say 不是虚函数。所以编译器翻译a.say()时 延Apple继承树向上,找到最近的say成员函数定义,并翻译为call Apple::say(a),得到期望输出 “I‘m an apple!”

1
2
3
4
5
int main(){
Apple a;
Fruit *fPtr = &a;
fPtr->say();
}

基类指针指向派生类对象,这对于实现多态性的行为是至关重要的。

派生类可以重写从基类继承过来的函数,上述 main 函数的输出为:I’m a fruit!

C++默认这样的成员函数“重写”为隐藏。 由于 Fruit 不是多态类型,因此编译器使用*fPtr 的静态类型解释 fPtr->say(),即解释为Fruit::Say(fptr)

动态绑定

仅需要在基类的成员函数前面加上virtual关键字,就能把一个函数声明为虚函数。该类及其子类都是多态类型。(注意:apple也是多态类型)

当 多态类型指针(或引用)调用虚函数时,则产生多态现象,即调用指针所指向的对象的成员函数,上述代码输出“I’m an apple!”

纯虚函数与抽象类

纯虚函数:用=0 作为虚函数声明的后缀,表明该函数是纯虚函数

  • 纯虚函数可以(不建议)给定义。(若纯虚函数是析构函数则必须提供)

抽象类(Abstract Class):定义或继承了至少一个纯虚函数的类。

  • 抽象类不能被实例化
  • 抽象函数的子类是抽象类,除非所有的纯虚函数都被覆写(override)为非纯虚函数
1
2
3
4
class Animal{
public:
virtual void eat() = 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
struct Abstract
{
virtual void f() = 0; // 纯虚
}; // "Abstract" 是抽象的

struct Concrete : Abstract
{
void f() override {} // 非纯虚
virtual void g(); // 非纯虚
}; // "Concrete" 不是抽象的

struct Abstract2 : Concrete
{
void g() override = 0; // 纯虚覆盖函数
}; // "Abstract2" 是抽象的

int main()
{
// Abstract a; // 错误:不能创建抽象类的对象
Concrete b; // OK
Abstract& a = b; // OK:到抽象基类的引用
a.f(); // 虚派发到 Concrete::f()
// Abstract2 a2; // 错误:不能创建抽象类的对象(g() 的最终覆盖函数是纯虚的)
}

抽象类语法与语义:

  • 仅能作为基类指针或引用,因为不可能存在抽象对象
    • 能声明为指针或引用,指代自己派生类对象
    • 不能定义抽象类的对象
    • 不能显示转为抽象类对象
    • 不能作为函数参数类型或者返回的值

虚析构函数

程序回避了 new 包含虚析构函数的类 这样的语句,因为

  • 在抽象与接口案例中,多态类型指针必须动态转换为对象实际类型指针才能正确执行对象析构过程。
  • 若基类声明其析构函数为 virtual,则派生的析构函数始终覆盖它。这使得可以通过指向基类的指针 delete 动态分配的多态类型对象。
  • 任何包含虚函数的基类的析构函数必须为公开且虚,或受保护且非虚。否则很容易导致内存泄漏

RTTI概念

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

  • C 是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。
  • 面向对象程序设计中多态性要求,C++中的指针或引用本身的类型,可能与它实际指代(指向或引用)的类型并不一致,需要在运行时将一个多态指针转换为其实际指向对象的类型。
  • RTTI 提供了两个非常有用的操作符:typeiddynamic_cast
    • typeid 操作符,返回指针和引用所指的实际类型(type_info 对象)
    • dynamic_cast 操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用

typeid 运算符

查询类型的信息。用于必须知晓多态对象的动态类型的场合以及静态类型鉴别

  • 在使用 typeid 前,必须包含头文件 <typeinfo>
  • typeid 返回 std::type_info 对象,它常用的有 ==、!= 运算符 和 name() 成员

语法1:typeid ( 类型 )

  • 指代一个表示 类型 类型的 std::type_info 对象。若 类型 为引用类型,则结果所指代的std::type_info 对象表示被引用的类型。

语法2:typeid ( 表达式 )

  • 若 表达式 为标识某个多态类型(即声明或继承至少一个虚函数的类)对象的泛左值表达式,则 typeid 表达式对该表达式求值,然后指代表示该表达式动态类型的 std::type_info 对象。
  • 若 表达式 不是多态类型的泛左值表达式,则 typeid 不对该表达式求值,而是由编译静态推导表达式静态类型的 std::type_info 对象

dynamic_cast 类型转换运算符

沿继承层级向上、向下及侧向,安全地转换到其他类的指针和引用。

语法:dynamic_cast <新类型> ( 表达式 )

若转型成功,则 dynamic_cast 返回 新类型 类型的值。

若转型失败,

  • 且 新类型 是指针类型,则它返回该类型的空指针。
  • 且 新类型 是引用类型,则它抛出与类型 std::bad_cast 的处理块匹配的异常。

指定某个虚函数不能在子类中被覆盖,或者某个类不能被子类继承。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Base
{
virtual void foo();
};

struct A : Base
{
void foo() final; // Base::foo 被覆盖而 A::foo 是最终覆盖函数
void bar() final; // 错误:bar 非虚,因此它不能是 final 的
};

struct B final : A // struct B 为 final
{
void foo() override; // 错误:foo 不能被覆盖,因为它在 A 中是 final 的
};

struct C : B{}; // 错误:B 是 final 的

泛型编程

独立于任何特定数据类型的编程,这使得不同类型的数据(对象)可以被相同的代码操作。

C++中,使用模板(template)来泛型编程,包括

  • 函数模板

  • 类模板

实例化(Instantiation):由编译器将通用模板代码转换为处理特定类型数据的实例代码的过程称为实例化

  • 当从通用代码创建实例代码时,具体数据类型才被确定
  • 泛型编程是一种编译时多态性(静态多态)。其中,数据类型本身是参数化的,因而程序具有多态性特征

函数模板

作用:使用相同的处理过程,处理不同类型的数据。能够减少代码, 甚至能处理编程时未知的数据类型。

函数模板的一般形式

1
2
3
4
5
template <模板形参表>
返回值类型 函数名(形式参数列表)
{
函数体语句
}

注:形式参数列表中必须包含模板形参表中出现的所有模板形参

交换函数的含模板的代码

1
2
3
4
5
6
7
8
template<typename T>
void Swap(T& v1, T& v2)
{
T temp;
temp = v1;
v1 = v2;
v2 = temp;
}

对上述函数模板进行实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
std::string s1("rabbit"), s2("bear");
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;

// 调用函数模板的实例Swap(string&, string&)
Swap(s1, s2);
// 调用函数模板的实例Swap(int&, int&)
Swap(iv1, iv2);
// 调用Swap的实例Swap(double&, double&)
Swap(dv1, dv2);
}

函数模板实例化

调用函数模板的过程:

  1. 模板实参推断(template argument deduction):

    编译器根据函数调用中所给出的实参的类型,确定相应的模板实参。

    显式实例化,显式指定模板形参的类型

  2. 函数模板的实例化(instantiation):

    模板实参确定之后,编译器就使用模板实参代替相应的模板形参,产生并编译函数模板的一个特定版本(称为函数模板的一个实例(instance))

    注意:此过程中不进行常规隐式类型转换

函数模板特化

  • 情形:在函数模板当中有些特殊的类型,当想要针对特殊的类型进行一些特殊的处理,这时候就可以用函数模板的特化

  • 特化方法:在正常的函数模板下面接着编写代码,写一个空的template<>然后写个具体的函数代码来补充。如左图所示。

  • 效果:如实例所示,当传入的实参类型是int类型,就执行函数模板的特化部分,而非int类型执行正常的模板推断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    template <typename T>
    void Swap( T& v1, T& v2)
    {
    T temp;
    temp = v1;
    v1 = v2;
    v2 = temp;
    }
    template <>
    void Swap( int & v1, int & v2)
    {
    int temp;
    temp = v1;
    v1 = v2;
    v2 = temp;
    v1 += 10;
    v2 += 10;
    }

函数模板重载

  • 定义名字相同而函数形参表不同的函数模板
  • 或者定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为

函数调用的静态绑定规则(重载协议):

  1. 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
  2. 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步在步骤2中:首先匹配函数模板的特化,再匹配非指定特殊的函数模板
  3. 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹 配的函数则调用该函数。否则,进入第4步
  4. 提示编译错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 函数模板demoPrint
template <typename T>
void demoPrint(const T v1, const T v2){
cout << "the first version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}
// 函数模板demoPrint的特化
template <>
void demoPrint(const char v1, const char v2){
cout << "the specify special of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}
// 函数模板demoPrint重载的函数模板
template <typename T>
void demoPrint(const T v){
cout << "the second version of demoPrint()" << endl;
cout << "the argument: " << v << endl;
}
// 非函数模板demoPrint
void demoPrint(const double v1, const double v2){
cout << "the nonfunctional template version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}

int main()
{
/* 函数调用 */
string s1("rabbit"), s2("bear");
char c1('k'), c2('b');
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;
// 调用第一个函数模板
demoPrint(iv1, iv2);
// 调用第一个函数模板的特化
demoPrint(c1, c2);
// 调用第二个函数模板
demoPrint(iv1);
// 调用非函数模板
demoPrint(dv1, dv2);
// 隐式转换后调用非函数模板
demoPrint(iv1, dv2);
}

类模板

使用情景:定义可以存放任意类型对象的通用容器类 • 定义一个栈(stack)类,即可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素 • 定义一个队列(queue)类,即可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素

实现方式:为类声明一种模板,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型和用户自定义类型)

类模板的一般形式:

1
2
3
4
5
6
7
/* 类模板一般形式 */
template <模板形参表>
class 类模板名
{
类成员声明
...
}

在类模板外定义成员函数的一般形式:

1
2
3
4
5
6
7
/* 在类模板外定义成员函数的一般形式 */
template <模板形参表>
返回值类型 类模板名<模板形参名列表>::函数名(函数形参表)
{
函数实现
...
}

其中模板形参表的形式为:template <typename 类型参数1, typename 类型参数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
/* 类模板:使用链表实现的栈*/
template <typename ElementType>
class Stack {
public:
Stack();
~Stack();
void push(ElementType obj) throw(std::bad_alloc);
void pop() throw(std::logic_error);
ElementType getTop() const throw(std::logic_error);
bool isEmpty() const;
private:
/* 栈节点类型 */
struct Node
{
ElementType element; // 结点中存放的元素
Node* next;
// 指向下一结点的指针
};
Node* top;
// 栈顶节点
};
/* 向栈内压入元素 */
template <typename ElementType>
void Stack<ElementType>::push( ElementType obj ) throw(std::bad_alloc)
{
Node* temp;
try {
temp = new Node;
temp -> element = obj;
temp -> next = top;
top = temp;
}
catch (std::bad_alloc e) { // 内存分配失败时进行异常处理
throw;
// 重新抛出异常
}
}
/* 从栈顶弹出元素 */
template <typename ElementType>
void Stack<ElementType>::pop() throw(std::logic_error)
{
Node* temp;
if (top != NULL) {
temp = top;
top = top -> next;
delete temp;
}
else { // 栈为空时抛出异常
throw std::logic_error("pop from empty Stack");
}
}

/* 获取栈顶元素 */
template <typename ElementType>
ElementType Stack<ElementType>::getTop() const throw(std::logic_error)
{
if (top != NULL) {
return top->element;
}
else { //栈为空时抛出异常
throw std::logic_error("getTop from empty Stack");
}
}
/* 判断栈是否为空 */
template <typename ElementType>
bool Stack<ElementType>::isEmpty() const
{
return top == NULL;
}
/* 主函数 */
int main()
{
Stack<int> stack;
// 实例化一个保存int型元素的栈
for (int i = 1; i < 9; i++) // 向栈中压入8个元素
stack.push(i);
while (!stack.isEmpty()) {
// 栈不为空时循环
cout << stack.getTop() << " "; // 显示栈顶元素
stack.pop();
// 弹出栈顶元素
}
}

非函数模板形参

  • 两类模板形参:类型模板形参和非类型模板形参

  • 非类型模板形参:

    • 相当于模板内部的常量

    • 形式上类似于普通的函数形参

    • 对模板进行实例化时,非类型形参由相应模板实参的值代替

    • 对应的模板实参必须是 编译时常量表达式

示例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
/* 类模板:使用数组实现的栈*/
template <typename ElementType, int N>
class Stack {
public:
Stack();
~Stack();
void push(ElementType obj);
void pop();
ElementType getTop() const;
bool isEmpty() const;
private:
/* 栈节点类型 */
ElementType elements[N];
int count;
};
/* 主函数 */
int main()
{
Stack<int, 10> stack;
// 实例化一个保存int型元素的栈
for (int i = 1; i < 9; i++) // 向栈中压入8个元素
stack.push(i);
while (!stack.isEmpty()) {
// 栈不为空时循环
cout << stack.getTop() << " "; // 显示栈顶元素
stack.pop();
// 弹出栈顶元素
}
}

函数模板的非类型模板形参

打印函数

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
/* 非类型模板形参实例2 */
template <typename T, int N>
void printValues(T (&arr)[N]) {
for (int i = 0; i != N; ++i)
cout<< arr[i] << endl;
}
int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
// 生成函数实例printValues(int (&) [6])
printValues(intArr);
// 生成函数实例printValues(double (&) [4])
printValues(dblArr);
return 0;
}

/* 非类型模板形参实例2 */
template <typename T, int N>
void printValues(T (*arr)[N]) {
for (int i = 0; i != N; ++i)
cout<< (*arr)[i] << endl;
}
int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
// 生成函数实例printValues(int (&) [6])
printValues(&intArr);
// 生成函数实例printValues(double (&) [4])
printValues(&dblArr);
return 0;
}

STL

C++标准库的一部分

容器

容器是储存其他对象的对象

容器分为四种: • 序列式容器 • 关联式容器 • 无序关联式容器 • 容器适配器 简单来说,每个容器提供了一种适配大部分类型的数据结构

迭代器:迭代器是每种容器各自定义的一个或多个不同于容器的类,比如 vector<T>::iterator,它主要用于访问、修改、增加、删除容器中的元素。

按照功能的不同,C++17之前的迭代器分为: LegacyInputIterator(输入迭代器), LegacyOutputIterator(输出迭代器), LegacyForwardIterator(单向迭代器), LegacyBidirectionalIterator(双向迭代器), LegacyRandomAccessIterator(随机迭代器) 五种,C++17加入了 LegacyContiguousIterator (连续迭代器)

序列式容器类型

构造函数

访问元素

image-20230615151216828

注:对于list容器不支持随机访问。

使用迭代器访问

image-20230615151337749

插入元素

还有:c.emplace_back(t)

删除元素

容器比较

image-20230615152240917

容量操作

容器的赋值和交换

image-20230615152414454

关联容器

std::pair

std::map的某些函数使用了<utility>中的类std::pair,pair<T1, T2>代表一个由类型T1和类型T2组成的有序对。 可以直接用构造函数 std::pair<T1, T2> (v1, v2) 进行构造,也可以用make_pair进行构造: auto p = make_pair(v1, v2); 通过p.first访问第一个元素,通过p.second访问第二个元素

map

map的构造函数

image-20230615152726812

向map中插入元素

1
2
3
4
std::map<string, int> m = { { “hello”, 1 } };
m.insert({ “h”, 10 });
m.insert(std::pair{"Kageyama", 180.6});
m.emplace("hh", 233);

最简便的方法是直接用operator[]来插入元素:m["me"] = 10

在 map 中查找元素

image-20230615152959585

在map中删除元素

image-20230615153058006

multimap
  • 不支持下标操作
  • insert 操作每调用一次都会增加新的元素(multimap容器中,键相同的元素相邻存放)。
  • 以键值为参数的erase操作删除该键所关联的所有元素,并返回被删除元素的数目。
  • count 操作返回指定键的出现次数。
  • find 操作返回的迭代器指向与被查找键相关联的第一个元素。
  • 结合使用count和find操作依次访问,multimap容器中与特定键关联的所有元素。

image-20230615153510895

set

map支持的操作set基本上都支持,但有区别。如下: 1)不支持下标操作。 2)没有定义mapped_type类型 3)set容器定义的value_type类型不是pair类型,而是与key_type相同,指的都set中元素的类型。

1
2
3
4
std::set<int> s = { 1, 2, 3, 4 };
auto iter1 = s.find(3);
auto iter2 = s.insert(3); // 返回值为指向之前的3的迭代器,没有实际插入发生
auto b = iter1 == iter2; // b 为 true

适配器

容器适配器提供顺序容器之上的不同功能接口(界面)。

image-20230615153803274

队列 (queue) 功能接口(操作)

还有emplace()

优先队列(priority_queue)的功能接口(操作)

适配器与容器

  • 标准库中定义的容器适配器都是基于顺序容器建立的

  • 程序员在创建适配器对象时可以选择相应的基础容器类

    • stack 适配器可以建立在 vectorlistdeque 容器

      原因:高效支持尾部 push(item), pop()

    • queue 适配器只能建立在 listdeque 容器上。

      原因:高效支持尾部 push(item), 首部pop()

    • priority_queue 适配器只能建立在 vectordeque 容器上。

      原因:高效支持堆排序

  • 如果创建适配器对象时不指定基础容器,则

    • stackqueue 默认采用deque实现
    • priority_queue 则默认采用 vector 实现。

迭代器

类别

image-20230615155036568

异常处理

异常处理概述

程序终止

  • 正常终止:执行正常结束而终止
  • 异常终止:程序执行中发生错误或特殊事件而终止
    • 可预测的错误
    • 用户自己定义的错误
    • 难以预测的错误

异常处理(exception handling)机制的基本思想

  • 采用结构化方法对程序的运行时错误进行显示管理:
    • 目标1:处理的是可预料的错误或特殊事件
    • 目标2:将程序中的正常处理代码与异常处理代码显示区分开来,提高程序的可读性

结构化方法的两种含义

  • 结构化定义异常
    • 将异常种类定义为树状结构
  • 结构化处理异常(异常检测 与 异常处理 分离)
    • 异常检测:
      • 异常检测部分检测到异常的存在时,抛出一个异常对象给异常处理代码。
    • 异常处理:
      • 在程序或函数特定位置,集中捕获异常对象,再处理异常。
    • 通过该异常对象,独立开发的异常检测部分和异常处理部分就能够就程序执行期间出现的异常情况进行通信。
  • 标题: C++ 基础知识学习
  • 作者: 卡布叻_米菲
  • 创建于 : 2023-03-01 17:30:39
  • 更新于 : 2024-02-08 13:09:05
  • 链接: https://carolinebaby.github.io/2023/03/01/C++基础知识/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8