《C++面向对象程序设计》✍学习笔记

news2024/11/15 10:02:55

C++的学习重点

C++ 这块,重点需要学习的就是一些关键字、面向对象以及 STL 容器的知识,特别是 STL,还得研究下他们的一些源码,下面是一些比较重要的知识:

  • 指针与引用的区别,C 与 C++ 的区别,struct 与 class 的区别;
  • struct 内存对齐问题,sizeof 与 strlen 区别;
  • 面向对象的三大特性:封装、继承、多态;
  • 类的访问权限:private、protected、public;
  • 类的构造函数、析构函数、赋值函数、拷贝函数;
  • 移动构造函数与拷贝构造函数对比;
  • 内存分区:全局区、堆区、栈区、常量区、代码区;
  • 虚函数实现动态多态的原理、虚函数与纯虚函数的区别;
  • 深拷贝与浅拷贝的区别;
  • 一些关键字:static, const, extern, volatile 等;
  • 四种类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast;
  • 静态与多态:重写、重载、模板;
  • 四种智能指针及底层实现:auto_ptr、unique_ptr、shared_ptr、weak_ptr;
  • 右值引用;
  • std::move函数;
  • 迭代器原理与迭代器失效问题;
  • 一些重要的 STL:vector, list, map, set 等。
  • 容器对比,如 map 与 unordered_map 对比,set 与 unordered_set 对比,vector 与 list 比较等。
  • STL容器空间配置器等等。

目录

C++的学习重点

C++面向对象程序设计

一、面向对象程序设计

二、C++基础 

2.1 C++的产生和特点

2.2 一个简单的C++示例程序

2.3 C++在非面向对象方面对C语言的扩充

2.3.1输入和输出

2.3.2 const 修饰符

2.3.3 void 型指针

2.3.4 内联函数

2.3.5 带有默认参数值的函数

2.3.6 函数重载

2.3.7 作用域标识符"::"

2.3.8 强制类型转换

2.3.9 new和delete运算符

2.3.10 引用

三、类和对象

3.1 类的构成

3.2 成员函数的定义

3.2.1 普通成员函数的定义

3.2.2 内联成员函数的定义

3.3 对象的定义和使用

3.3.1 对象的定义

3.3.2 对象中成员的访问

3.3.3 类的作用域和类成员的访问属性

3.3.4 对象赋值语句

3.4 构造函数与析构函数

3.4.1 构造函数

3.4.2 成员初始化列表

3.4.3 带默认参数的构造函数

3.4.4 析构函数

3.4.5 默认的构造函数和析构函数

3.4.6 构造函数的重载

3.4.7 拷贝构造函数

3.4.8 浅拷贝和深拷贝

3.5 自引用指针this

3.6 对象数组与对象指针 

3.7 string类

3.8 向函数传递对象

3.9 静态成员

3.10 友元

3.10.1 友元函数

3.10.2 友元类

3.11 类的组合

3.12 共享数据的保护

3.12.1 常引用

 3.12.2 常对象

 3.12.3 常对象成员

 四、继承与派生

4.1 继承与派生的概念

4.2 派生类的构造函数和析构函数

4.3 调整基类成员在派生类中的访问属性的其他方法

4.4 多继承 

 4.5 虚基类

4.6 赋值兼容规则 

五、多态性与虚函数

5.1 多态性概述

5.2 虚函数

5.4 纯虚函数

5.5 抽象类

 5.6 示例:利用多态计算面积

六、运算符重载

6.1 运算符重载概述

6.2 +号运算符重载

6.3 可重载运算符/不可重载运算符

七、函数模板与类模板

7.1 模板的概念

7.2 函数模板

7.3 类模板

八、C++的输入和输出

8.1 C++为何建立自己的输入/输出系统

8.2 C++的流库及其基本结构

8.3 预定义的流对象

8.4 输入/输出流的成员函数

8.5 预定义类型输入/输出的格式控制

8.6 文件的输入/输出

8.7 文件的打开与关闭 

8.8 文本文件的读/写 

8.9 二进制文件的读写

九、异常处理和命名空间

9.1 异常处理

9.2 命名空间和头文件命名规则

十、STL标准模板库

10.1 Vector

10.2 list容器

10.3 stack

10.4 queue

10.5 优先队列priority_queue

10.6 双端队列deque

10.7 set

10.8 map


C++面向对象程序设计

下面是一些对这本书中内容的总结

一、面向对象程序设计

面向对象程序设计(Object-Oriented ProgrammingOOP)是一种新的程序设计范型。程序设计范型是指设计程序的规范、模型和风格,它是一类程序设计语言的基础。

面向过程程序设计范型是使用较广泛的面向过程性语言,其主要特征是:程序由过程定义过程调用组成(简单地说,过程就是程序执行某项操作的一段代码,函数就是最常用的过程)。

面向对象程序的基本元素是对象,面向对象程序的主要结构特点是:第一,程序一般由类的定义和类的使用两部分组成;第二,程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后,启动有关方法完成相应的操作。

二、C++基础 

2.1 C++的产生和特点

C++是美国贝尔实验室的Bjarne Stroustrup博士在C语言的基础上,弥补了C语言存在的一些缺陷,增加了面向对象的特征,于1980年开发出来的一种面向过程性与面向对象性相结合的程序设计语言。最初他把这种新的语言称为“含类的C”,到1983年才取名为C++。

相比C语言,C++的主要特点是增加了面向对象机制。

2.2 一个简单的C++示例程序
#include <iostream>   //编译预处理命令,用于包含标准输入输出流的头文件。这样程序就可以使用输入输出流对象 cin 和 cout 了
using namespace std;    //使用命名空间,它告诉编译器使用标准命名空间 std 中的所有成员,这样就可以直接使用 cin、cout 等标准输入输出对象,而不需要在每个对象前面加上 std::

int add(int a, int b);       //函数原型说明,用于告诉编译器在函数 main() 中会使用一个名为 add 的函数,该函数接受两个整型参数并返回一个整型结果

int main()  //主函数,程序从这里开始执行
{
	int x, y;      // 声明了两个整型变量 x 和 y,用于存储用户输入的两个整数
	cout << "Enter two numbers: " << endl;        //使用 cout 对象输出提示信息,提示用户输入两个数字
	cin >> x;
	cin >> y;         //使用 cin 对象从标准输入流(通常是键盘输入)读取用户输入的两个整数,并将它们存储到变量 x 和 y 中
	int sum = add(x, y);          //调用名为 add 的函数,并将其返回值赋给 sum 变量。这里将用户输入的两个整数作为参数传递给 add 函数
	cout << "The sum is : " << sum << '\n';      //使用 cout 对象输出计算得到的和 sum。
	return 0;            //返回主函数的结束状态,0 表示程序正常结束
}

int add(int a, int b) //定义add()函数,函数值为整型, 接受两个整型参数 a 和 b,并返回它们的和
{
	return a + b;
}

把这个程序逐步调试一遍就可以很清楚的理解每一步的作用

2.3 C++在非面向对象方面对C语言的扩充

2.3.1输入和输出
int i;
float f;
cin >> i;
cout << f;
------------
scanf("%d", &i);
printf("%f", f);
----------------
连续读入
cin >> a >> b >> c;

  • cin

cin 是 C++ 中用于从标准输入流(通常是键盘输入)读取数据的输入流对象。它是 C++ 标准库中的一部分,用于实现基于流的输入操作。

通过使用 cin,您可以从用户键盘输入数据,并将其存储在程序中的变量中,以供后续使用。它是 C++ 中常用的标准输入方式之一,通常与流操作符 >> 结合使用。

在默认情况下,运算符“>>”将跳过空白符,然后读入后面与变量类型相对应的值。因此,给一组变量输入值时可用空格符、回车符、制表符将输入的数据间隔开。

当输入字符串(即类型为string的变量)时,提取运算符“>>”的作用是跳过空白字符,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标志‘\0’。

  • cout

cout 是 C++ 中用于向标准输出流(通常是控制台)输出数据的输出流对象。它是 C++ 标准库中的一部分,用于实现基于流的输出操作。

通过使用 cout,您可以在程序执行时将数据显示在屏幕上,以便用户查看或调试。它是 C++ 中常用的标准输出方式之一,通常与流操作符 << 结合使用。

  • 从2.2可以看出,C++允许在代码块中的任何地方声明局部变量

2.3.2 const 修饰符

在C语言中,习惯使用#define来定义常量,例如#define PI 3.14,C++提供了一种更灵活、更安全的方式来定义常量,即使用const修饰符来定义常量。例如const float PI = 3.14;

const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针、常指针和指向常量的常指针。

  • 指向常量的指针:一个指向常量的指针变量。
const int* ptr;

这里 ptr 是一个指向常量整数的指针,表示指针所指向的值是常量,
不能通过 ptr 修改所指向的值,但可以修改指针本身,让其指向其他地方。
  • 常指针:将指针变量所指的地址声明为常量
int x = 5;
int* const ptr = &x;

这里 ptr 是一个常量指针,表示指针本身是常量,
不能通过 ptr 修改指针指向的地址,但可以通过 *ptr 修改指针所指向的值。
  • 指向常量的常指针:这个指针所指的地址不能改变,它所指向的地址中的内容也不能改变。
const int* const ptr;

这里 ptr 是一个指向常量整数的常量指针,表示指针本身和所指向的值都是常量,
既不能通过 ptr 修改指针指向的地址,也不能通过 *ptr 修改指针所指向的值。
  • 常量引用参数:  函数不会修改指针所指向的值
void foo(const int* ptr);

这里 ptr 是一个指向常量整数的指针,表示 foo 函数不会修改指针所指向的值。
  • 指向常量的指针引用:  函数不能修改指针指向的地址
void foo(int* const & ptr);

这里 ptr 是一个常量指针的引用,表示 foo 函数不能修改指针指向的地址。

说明:

  1. 如果用const定义整型常量,关键字可以省略。即 const int MAX_VALUE = 100;
    与 int MAX_VALUE = 100 等价;
  2. 常量一旦被建立,在程序的任何地方都不能再更改。
  3. 与#define不同,const定义的常量可以有自己的数据类型。
  4. 函数参数也可以用const说明,用于保证实参在该函数内不被改动。

2.3.3 void 型指针

void 型指针是一种特殊类型的指针,在 C 和 C++ 中都存在。它是一种通用的指针类型,可以指向任何类型的数据,但不能直接通过指针访问或修改其所指向的数据。

在 C 语言中,void 型指针通常用于处理不同类型的数据或实现通用的数据处理函数。在 C++ 中,由于引入了强类型的特性,void 型指针的使用不如在 C 中那么常见,但它仍然有其用处,尤其是在与 C 代码进行交互、实现泛型编程时

下面是一些关于 void 型指针的重要特点:

  1. 通用性:void 型指针可以指向任何类型的数据,无论是基本数据类型、自定义类型还是函数等。

  2. 类型安全性:尽管 void 型指针可以指向任何类型的数据,但由于其无法直接访问数据,因此编译器无法执行类型检查。这可能导致在使用 void 型指针时存在潜在的类型错误风险,需要程序员自行确保指针的正确使用。

  3. 无法解引用:void 型指针不能直接解引用,因为它没有指定所指向数据的类型。在解引用之前,通常需要将 void 型指针转换为具体类型的指针。

    void* pc;
    int i = 123;
    char c = 'a';
    pc = &i;
	cout << pc << endl;         //输出指针地址006FF730
	cout << *(int*)pc << endl;  //输出值123
    pc = &c;
	cout << *(char*)pc << endl; //输出值a

2.3.4 内联函数

在函数名前冠以关键字inline,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。

说明:

  • 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
  • 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
  • 使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用
#include <iostream>
using namespace std;

inline double circle(double r)  //内联函数
{
	double PI = 3.14;
	return PI * r * r;
}

int main() 
{
	for (int i = 1; i <= 3; i++)
		cout << "r = " << i << " area = " << circle(i) << endl;
	return 0;
}

输出
r = 1 area = 3.14 
r = 2 area = 12.56
r = 3 area = 28.26

使用内联函数替代宏定义,能消除宏定义的不安全性

2.3.5 带有默认参数值的函数

当进行函数调用时,编译器按从左到右的顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。

#include <iostream>
using namespace std;

// 带有默认参数值的函数声明
void printInfo(string name = "Guest", int age = 30, string country = "Unknown") {
    cout << "Name: " << name << ", Age: " << age << ", Country: " << country << endl;
}

int main() {
    // 不提供参数值,将使用默认值
    printInfo(); // 输出:Name: Guest, Age: 30, Country: Unknown

    // 提供部分参数值
    printInfo("Alice"); // 输出:Name: Alice, Age: 30, Country: Unknown

    // 提供所有参数值
    printInfo("Bob", 25, "USA"); // 输出:Name: Bob, Age: 25, Country: USA

    return 0;
}
  • 在函数原型中,所有取默认值的参数都必须出现在不取默认值的参数的右边
  • 在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值

2.3.6 函数重载

在C++中,用户可以重载函数。这意味着,在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名

#include <iostream>
using namespace std;

int add(int x, int y)
{
	return x + y;
}

double add(double x, double y)
{
	return x + y;
}

int add(int x, int y, int z)
{
	return x + y + z;
}

int main() 
{
	int a = 3, b = 5, c = 7;
	double x = 10.334, y = 8.9003;
	cout << add(a, b) << endl;
	cout << add(x, y) << endl;
	cout << add(a, b, c) << endl;
	return 0;
}

说明:

  • 调用重载函数时,函数返回值类型不在参数匹配检查之列。因此,若两个函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
int mul(int x, int y);
double mul(int x, int y);
  • 函数的重载与带默认值的函数一起使用时,有可能引起二义性。
void Drawcircle(int r = 0, int x = 0, int y = 0);
void Drawcircle(int r);
Drawcircle(20);
  • 在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。
void f_a(int x);
void f_a(long x);
f_a(20.83);

2.3.7 作用域标识符"::"

通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。

如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。

#include <iostream>
using namespace std;

int value;   //定义全局变量value

int main() 
{
	int value;  //定义局部变量value
	value = 100;
	::value = 1000;
	cout << "local value : " << value << endl;
	cout << "global value : " << ::value << endl;
	return 0;
}

2.3.8 强制类型转换

可用强制类型转换将不同类型的数据进行转换。例如,要把一个整型数(int)转换为双精度型数(double),可使用如下的格式:

int i = 10;
double x = (double)i;
或
int i = 10;
double x = double(i);

以上两种方法C++都能接受,建议使用后一种方法。

2.3.9 new和delete运算符

程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()free()来进行动态内存管理。C++则提供了运算符newdelete来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。

指针变量名 = new 类型
    int *p;
    p = new int;
delete 指针变量名
    delete p;

下面对new和delete的使用再做一下几点说明:

  • 用运算符new分配的空间,使用结束后应该用也只能用delete显式地释放,否则这部分空间将不能回收而变成死空间。

  • 在使用运算符new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针NULL)。

  • 使用运算符new可以为数组动态分配内存空间,这时需要在类型后面加上数组大小。

指针变量名 = new 类型名[下标表达式];
int *p = new int[10];

       释放动态分配的数组存储区时,可使用delete运算符。 

delete []指针变量名;
delete p;
  • new 可在为简单变量分配空间的同时,进行初始化

指针变量名 = new 类型名(初值);
int *p;
p = new int(99);
···
delete p;

2.3.10 引用

引用reference)是C++对C的一个重要扩充。变量的引用就是变量的别名,因此引用又称别名

类型 &引用名 = 已定义的变量名

引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。实际上,编译系统使引用和其代表的变量具有相同的地址。

#include <iostream>
using namespace std;
int main() 
{
	int i = 10;
	int &j = i;
	cout << "i = " << i << " j = " << j << endl;
	cout << "i的地址为 " << &i << endl;
	cout << "j的地址为 " << &j << endl;
	return 0;
}

上面代码输出i和j的值相同,地址也相同。

  • 引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值。
  • 为引用提供的初始值,可以是一个变量或者另一个引用。
  • 指针是通过地址间接访问某个变量,而引用则是通过别名直接访问某个变量。

引用作为函数参数、使用引用返回函数值

#include <iostream>
using namespace std;

void swap(int &a, int &b)
{
	int t = a;
	a = b;
	b = t;
}

int a[] = {1, 3, 5, 7, 9};

int& index(int i)
{
	return a[i];
}

int main() 
{
	int a = 5, b = 10;
	//交换数字a和b
	swap(a, b);
	cout << "a = " << a << " b = " << b << endl;
	cout << index(2) << endl;   //等价于输出元素a[2]的值
	index(2) = 100;             //等价于将a[2]的值赋为100;
	cout << index(2) << endl;
	
	return 0;
}

对引用的进一步说明

  • 不允许建立void类型的引用
  • 不能建立引用的数组
  • 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
  • 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
  • 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。

三、类和对象

3.1 类的构成

类声明中的内容包括数据和函数,分别称为数据成员和成员函数。按访问权限划分,数据成员和成员函数又可分为共有、保护和私有3种。

class 类名{
    public:
        公有数据成员;
        公有成员函数;
    protected:
        保护数据成员;
        保护成员函数;
    private:
        私有数据成员;
        私有成员函数;
};

如成绩类

class Score{
    public:
    void setScore(int m, int f);
    void showScore();
    private:
    int mid_exam;
    int fin_exam;
};
  • 对一个具体的类来讲,类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
  • 类声明中的关键字private、protected、public可以任意顺序出现。
  • 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
  • 不能在类声明中给数据成员赋初值。

3.2 成员函数的定义

3.2.1 普通成员函数的定义

在类的声明中只给出成员函数的原型,而成员函数的定义写在类的外部。这种成员函数在类外定义的一般形式是:

返回值类型 类名::成员函数名(参数表){    函数体}

例如,表示分数的类Score可声明如下:

class Score{
public:
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

void Score::setScore(int m, int f) 
{
	mid_exam = m;
	fin_exam = f;
}

void Score::showScore()
{
	cout << "期中成绩: " << mid_exam << endl;
	cout << "期末成绩:" << fin_exam << endl;
}

3.2.2 内联成员函数的定义
  • 隐式声明:将成员函数直接定义在类的内部
class Score{
public:
	void setScore(int m, int f)
	{
		mid_exam = m;
		fin_exam = f;
	}
	void showScore()
	{
		cout << "期中成绩: " << mid_exam << endl;
		cout << "期末成绩:" << fin_exam << endl;
	}
private:
	int mid_exam;
	int fin_exam;
};
  • 显式声明:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。
class Score{
public:
	inline void setScore(int m, int f);
	inline void showScore();
private:
	int mid_exam;
	int fin_exam;
};

inline void Score::setScore(int m, int f) 
{
	mid_exam = m;
	fin_exam = f;
}

inline void Score::showScore()
{
	cout << "期中成绩: " << mid_exam << endl;
	cout << "期末成绩:" << fin_exam << endl;
}

说明:在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件(或同一个头文件)中,否则编译时无法进行代码置换。

3.3 对象的定义和使用

通常把具有共同属性和行为的事物所构成的集合称为类。

类的对象可以看成该类类型的一个实例,定义一个对象和定义一个一般变量相似。

3.3.1 对象的定义
  • 在声明类的同时,直接定义对象
class Score{
public:
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
}op1, op2;
  • 声明了类之后,在使用时再定义对象
  Score op1, op2;

3.3.2 对象中成员的访问
对象名.数据成员名对象名.成员函数名[(参数表)]op1.setScore(89, 99);
op1.showScore();

说明

  • 在类的内部所有成员之间都可以通过成员函数直接访问,但是类的外部不能访问对象的私有成员。

  • 在定义对象时,若定义的是指向此对象的指针变量,则访问此对象的成员时,不能用“.”操作符,而应该使用“->“操作符。如

	Score op, *sc;
	sc = &op;        //& 运算符用于取地址操作,即返回其操作数的内存地址
	sc->setScore(99, 100);
	op.showScore();

3.3.3 类的作用域和类成员的访问属性

私有成员只能被类中的成员函数访问,不能在类的外部,通过类的对象进行访问。

一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。

3.3.4 对象赋值语句
	Score op1, op2;
	op1.setScore(99, 100);
	op2 = op1;
	op2.showScore();

3.4 构造函数与析构函数

3.4.1 构造函数

构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。

class Score{
public:
	Score(int m, int f);  //构造函数
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f)   // 在这个构造函数的定义中,Score:: 表示该构造函数属于 Score 类。这样就告诉编译器这个构造函数是 Score 类的成员函数,并且需要关联到 Score 类中。
{
	mid_exam = m;
	fin_exam = f;
}

在建立对象的同时,采用构造函数给数据成员赋值,通常由以下两种形式

类名 对象名[(实参表)]
Score op1(99, 100);
op1.showScore();
类名 *指针变量名 = new 类名[(实参表)]
Score *p;
p = new Score(99, 100);
p->showScore();
-----------------------
Score *p = new Score(99, 100);
p->showScore();

说明:

  • 构造函数的名字必须与类名相同,否则编译程序将把它当做一般的成员函数来处理。
  • 构造函数没有返回值,在定义构造函数时,是不能说明它的类型的。
  • 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
  • 构造函数一般声明为共有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
  • 构造函数可以不带参数。

3.4.2 成员初始化列表

在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行。此外还可以用成员初始化列表实现对数据成员的初始化。

类名::构造函数名([参数表])[:(成员初始化列表)]
{
    //构造函数体
}
class A{
private:
	int x; 
	int& rx;         //rx 引用了另一个类成员 x,这意味着 rx 和 x 指向了同一个内存地址,并且通过 rx 可以直接访问和修改 x 的值
	const double pi;
public:
	A(int v) : x(v), rx(x), pi(3.14)    //成员初始化列表,在类的内部定义构造函数时,不需要使用作用域解析运算符 ::
	{	}
	void print()
	{
		cout << "x = " << x << " rx = " << rx << " pi = " << pi << endl;
	}
};

说明:类成员是按照它们在类里被声明的顺序进行初始化的,与它们在成员初始化列表中列出的顺序无关。

3.4.3 带默认参数的构造函数
#include <iostream>
using namespace std;

class Score{
public:
	Score(int m = 0, int f = 0);    //带默认参数的构造函数
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
	cout << "构造函数使用中..." << endl;
}

void Score::setScore(int m, int f) 
{
	mid_exam = m;
	fin_exam = f;
}

void Score::showScore()
{
	cout << "期中成绩: " << mid_exam << endl;
	cout << "期末成绩:" << fin_exam << endl;
}

int main() 
{
	Score op1(99, 100);
	Score op2(88);
	Score op3;
	op1.showScore();
	op2.showScore();
	op3.showScore();

	return 0;
}

3.4.4 析构函数

析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。析构函数有以下一些特点:

  1. 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
  2. 析构函数没有参数和返回值,也不能被重载,因此只有一个。
  3. 当撤销对象时,编译系统会自动调用析构函数。
class Score{
public:
	Score(int m = 0, int f = 0);
	~Score();       //析构函数
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
	cout << "构造函数使用中..." << endl;
}

Score::~Score()
{
	cout << "析构函数使用中..." << endl;
}

说明:在以下情况中,当对象的生命周期结束时,析构函数会被自动调用:

  • 如果定义了一个全局对象,则在程序流程离开其作用域时,调用该全局对象的析构函数。
  • 如果一个对象定义在一个函数体内,则当这个函数被调用结束时,该对象应该被释放,析构函数被自动调用。
  • 若一个对象是使用new运算符创建的,在使用delete运算符释放它时,delete会自动调用析构函数。

如下示例:

#include <iostream>
#include <string>

using namespace std;

class Student{
private:
	char *name;
	char *stu_no;
	float score;
public:
	Student(char *name1, char *stu_no1, float score1);
	~Student();
	void modify(float score1);
	void show();
};

Student::Student(char *name1, char *stu_no1, float score1)
{
	name = new char[strlen(name1) + 1];
	strcpy(name, name1);
	stu_no = new char[strlen(stu_no1) + 1];
	strcpy(stu_no, stu_no1);
	score = score1;
}

Student::~Student() 
{
	delete []name;
	delete []stu_no;
}

void Student::modify(float score1) 
{
	score = score1;
}

void Student::show()
{
	cout << "姓名: " << name << endl;
	cout << "学号: " << stu_no << endl;
	cout << "成绩:" << score << endl;
}

int main()
{
	Student stu("雪女", "2020199012", 99);
	stu.modify(100);
	stu.show();

	return 0;
}
3.4.5 默认的构造函数和析构函数

如果没有给类定义构造函数,则编译系统自动生成一个默认的构造函数。

说明:

  • 对没有定义构造函数的类,其公有数据成员可以用初始值列表进行初始化。

class A{
public:
	char name[10];
	int no;
};

A a = {"chen", 23};
cout << a.name << a.no << endl;
  • 只要一个类定义了一个构造函数(不一定是无参构造函数),系统将不再给它提供默认的构造函数。

每个类必须有一个析构函数。若没有显示地为一个类定义析构函数,编译系统会自动生成一个默认的析构函数。

3.4.6 构造函数的重载
#include <iostream>
using namespace std;

class MyClass {
public:
    int x, y;

    // 构造函数重载
    MyClass() {
        x = 0;
        y = 0;
    }

    MyClass(int a) {
        x = a;
        y = 0;
    }

    MyClass(int a, int b) {
        x = a;
        y = b;
    }

    void display() {
        cout << "x = " << x << ", y = " << y << endl;
    }
};

int main() {
    MyClass obj1; // 调用无参构造函数
    obj1.display(); // 输出:x = 0, y = 0

    MyClass obj2(5); // 调用带一个参数的构造函数
    obj2.display(); // 输出:x = 5, y = 0

    MyClass obj3(10, 20); // 调用带两个参数的构造函数
    obj3.display(); // 输出:x = 10, y = 20

    return 0;
}

注意:在一个类中,当无参数的构造函数和带默认参数的构造函数重载时,有可能产生二义性。

3.4.7 拷贝构造函数

拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。

拷贝构造函数具有以下特点:

  • 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
  • 拷贝构造函数只有一个参数,并且是同类对象的引用。
  • 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。

自定义拷贝构造函数

类名::类名(const 类名 &对象名) 
{
    拷贝构造函数的函数体;
}

class Score{
public:
	Score(int m, int f);  //构造函数
	Score();
	Score(const Score &p);  //拷贝构造函数
	~Score();               //析构函数
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
}

Score::Score(const Score &p)
{
	mid_exam = p.mid_exam;
	fin_exam = p.fin_exam;
}

调用拷贝构造函数的一般形式为:
    类名 对象2(对象1);
    类名 对象2 = 对象1;
Score sc1(98, 87);
Score sc2(sc1);    //调用拷贝构造函数
Score sc3 = sc2;   //调用拷贝构造函数

调用拷贝构造函数的三种情况:

  • 当用类的一个对象去初始化该类的另一个对象时;
Score obj1(80, 90); // 创建对象 obj1
Score obj2 = obj1;  // 直接调用拷贝构造函数,使用 obj1 的值来初始化 obj2
  • 当函数的形参是类的对象,调用函数进行形参和实参结合时;
void displayScore(Score s) {
    s.showScore(); // 在函数内部使用拷贝构造函数创建对象 s,并将原始对象的值复制给新对象 s
}

Score obj3(70, 85); // 创建对象 obj3
displayScore(obj3); // 将 obj3 作为参数传递给函数 displayScore
  • 当函数的返回值是对象,函数执行完成返回调用者时。
Score getScore() {
    Score temp(95, 88); // 创建临时对象 temp
    return temp; // 返回临时对象 temp
}

Score obj4 = getScore(); // 调用拷贝构造函数,将临时对象的值复制给 obj4

3.4.8 浅拷贝和深拷贝

浅拷贝,就是由默认的拷贝构造函数所实现的数据成员逐一赋值。通常默认的拷贝构造函数是能够胜任此工作的,但若类中含有指针类型的数据,则这种按数据成员逐一赋值的方法会产生错误。

class Student{
public:
    Student(char *name1, float score1);
    ~Student();
private:
    char *name;
    float score;
};

如下语句会产生错误
Student stu1("白", 89);
Student stu2 = stu1;

上述错误是因为stu1和stu2所指的内存空间相同,在析构函数释放stu1所指的内存后,再释放stu2所指的内存会发生错误,因为此内存空间已被释放。解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。

Student::Student(const Student& stu)
{
    name = new char[strlen(stu.name) + 1];
    if (name != 0) {
        strcpy(name, stu.name);
        score = stu.score;
    }
}

3.5 自引用指针this

this指针保存当前对象的地址,称为自引用指针

void Sample::copy(Sample& xy)
{
    if (this == &xy) return;
    *this = xy;
}

这段代码定义了一个名为 copy 的成员函数,它属于类 Sample。该函数的作用是将一个传入的 Sample 对象 xy 的值复制给当前对象 *this

3.6 对象数组与对象指针 

对象数组

类名 数组名[下标表达式]

#include <iostream>
using namespace std;

class MyClass {
public:
    int value;

    // 无参数构造函数
    MyClass() {
        value = 0;
    }

    // 带一个参数的构造函数
    MyClass(int v) {
        value = v;
    }

    // 带多个参数的构造函数
    MyClass(int v1, int v2) {
        value = v1 + v2;
    }

    void display() {
        cout << "Value: " << value << endl;
    }
};



用只有一个参数的构造函数给对象数组赋值
int main() {
    // 使用带一个参数的构造函数给对象数组赋值
    MyClass array1[3] = {MyClass(10), MyClass(20), MyClass(30)};

    // 显示数组元素的值
    for (int i = 0; i < 3; ++i) {
        array1[i].display();
    }

    return 0;
}


用不带参数和带一个参数的构造函数给对象数组赋值
int main() {
    // 使用不带参数的构造函数给对象数组赋值
    MyClass array2[3]; // 创建对象数组,自动调用不带参数的构造函数进行初始化

    // 显示数组元素的值
    for (int i = 0; i < 3; ++i) {
        array2[i].display();
    }

    return 0;
}


用带有多个参数的构造函数给对象数组赋值
int main() {
    // 使用带多个参数的构造函数给对象数组赋值
    MyClass array3[3] = {MyClass(1, 2), MyClass(3, 4), MyClass(5, 6)};

    // 显示数组元素的值
    for (int i = 0; i < 3; ++i) {
        array3[i].display();
    }

    return 0;
}

对象指针

每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的一半语法形式为:类名 *对象指针名

Score score;
Score *p;
p = &score;
p->成员函数();

用对象指针访问对象数组

Score score[2];
score[0].setScore(90, 99);
score[1].setScore(67, 89);

Score *p;
p = score;   //将对象score的地址赋值给p
p->showScore();
p++;    //对象指针变量加1
p->showSccore();

Score *q;
q =&score[1];   //将第二个数组元素的地址赋值给对象指针变量q
3.7 string类

C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’\0’(即以NULL结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。

C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:#include <string>

常用的string类运算符如下:

=、+、+=、==、!=、<、<=、>、>=、[](访问下标对应字符)、>>(输入)、<<(输出)
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string str1 = "ABC";
	string str2("dfdf");
	string str3 = str1 + str2;
	cout<< "str1 = " << str1 << "  str2 = " << str2 << "  str3 = " << str3 << endl;
	str2 += str2;
	str3 += "aff";
	cout << "str2 = " << str2 << "  str3 = " << str3 << endl;
	cout << "str1[1] = " << str1[1] << "  str1 == str2 ? " << (str1 == str2) << endl;
	string str = "ABC";
	cout << "str == str1 ? " << (str == str1) << endl;
	return 0;
}

3.8 向函数传递对象
  • 使用对象作为函数参数:对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过“传值调用”的方法传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象(实参本身)。
  • 使用对象指针作为函数参数:对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传值调用,即在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,形参对象指针所指的对象值的改变也同样影响着实参对象的值。
  • 使用对象引用作为函数参数:在实际中,使用对象引用作为函数参数非常普遍,大部分程序员喜欢使用对象引用替代对象指针作为函数参数。因为使用对象引用作为函数参数不但具有用对象指针做函数参数的优点,而且用对象引用作函数参数将更简单、更直接。
#include <iostream>
using namespace std;

class Point{
public:
	int x;
	int y;
	Point(int x1, int y1) : x(x1), y(y1)  //成员初始化列表
    { }
	int getDistance() 
	{
		return x * x + y * y;
	}
};

void changePoint1(Point point)    //使用对象作为函数参数
{
	point.x += 1;
	point.y -= 1;
}

void changePoint2(Point *point)   //使用对象指针作为函数参数
{
	point->x += 1;
	point->y -= 1;
}

void changePoint3(Point &point)  //使用对象引用作为函数参数
{
	point.x += 1;
	point.y -= 1;
}


int main()
{
	Point point[3] = {Point(1, 1), Point(2, 2), Point(3, 3)};
	Point *p = point;
	changePoint1(*p);
	cout << "the distance is " << p[0].getDistance() << endl;
	p++;
	changePoint2(p);
	cout << "the distance is " << p->getDistance() << endl;
	changePoint3(point[2]);
	cout << "the distance is " << point[2].getDistance() << endl;

	return 0;
}
3.9 静态成员

静态数据成员

在一个类中,若将一个数据成员说明为static,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。

定义静态数据成员的格式如下:static 数据类型 数据成员名;

说明:

静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。

静态数据成员的初始化与普通数据成员不同。静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。

静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“类名::”访问静态的数据成员。格式如下:类名::静态数据成员名

静态数据成员与静态变量一样,是在编译时创建并初始化。它在该类的任何对象被建立之前就存在。因此,共有的静态数据成员可以在对象定义之前被访问。对象定以后,共有的静态数据成员也可以通过对象进行访问。其访问格式如下

对象名.静态数据成员名;
对象指针->静态数据成员名;

静态成员函数

在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:

static 返回类型 静态成员函数名(参数表)

与静态数据成员类似,调用公有静态成员函数的一般格式有如下几种:

类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);

一般而言,静态成员函数不访问类中的非静态成员。若确实需要,静态成员函数只能通过对象名(或对象指针、对象引用)访问该对象的非静态成员。

下面对静态成员函数的使用再做几点说明:

  1. 一般情况下,静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
  2. 私有静态成员函数不能被类外部的函数和对象访问。
  3. 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的功能
  4. 编译系统将静态成员函数限定为内部连接,也就是说,与现行文件相连接的其他文件中的同名函数不会与该函数发生冲突,维护了该函数使用的安全性,这是使用静态成员函数的另一个原因。
  5. 静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公有的静态成员函数,使用如下格式较好:类名::静态成员函数名()
#include <iostream>
using namespace std;

class Score{
private:
	int mid_exam;
	int fin_exam;
	static int count;     //静态数据成员,用于统计学生人数
	static float sum;     //静态数据成员,用于统计期末累加成绩
	static float ave;     //静态数据成员,用于统计期末平均成绩
public:
	Score(int m, int f);
	~Score();
	static void show_count_sum_ave();   //静态成员函数
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
	++count;
	sum += fin_exam;
	ave = sum / count;
}

Score::~Score()
{

}

/*** 静态成员初始化 ***/
int Score::count = 0;
float Score::sum = 0.0;
float Score::ave = 0.0;

void Score::show_count_sum_ave()
{
	cout << "学生人数: " << count << endl;
	cout << "期末累加成绩: " << sum << endl;
	cout << "期末平均成绩: " << ave << endl;
}

int main()
{
	Score sco[3] = {Score(90, 89), Score(78, 99), Score(89, 88)};
	sco[2].show_count_sum_ave();
	Score::show_count_sum_ave();

	return 0;
}


//输出
学生人数: 3      
期末累加成绩: 276
期末平均成绩: 92 
学生人数: 3      
期末累加成绩: 276
期末平均成绩: 92 

sco[2].show_count_sum_ave() 中,sco[2] 表示数组 sco 中的第三个元素,即对象 sco[2] 。但是静态成员函数并不依赖于具体的对象,它可以通过类名直接调用,因此实际上 sco[2] 并不会影响静态成员函数的调用效果。只要使用了 Score::show_count_sum_ave(),就会输出类 Score 中的静态数据成员的统计信息,无论调用者是哪个对象。

3.10 友元

类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。

3.10.1 友元函数

友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。

在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。

1、将非成员函数声明为友元函数

#include <iostream>
using namespace std;
class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f);
	void showScore();
	friend int getScore(Score &ob);
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
}

int getScore(Score &ob)
{
	return (int)(0.3 * ob.mid_exam + 0.7 * ob.fin_exam);
}

int main()
{
	Score score(98, 78);
	cout << "成绩为: " << getScore(score) << endl;

	return 0;
}

说明:

  1. 友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
  2. 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
  3. 友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。

例子:一个函数同时定义为两个类的友元函数

#include <iostream>
#include <string>
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	friend void show(Score &sc, Student &st);
};

class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void show(Score &sc, Student &st);
};

void show(Score &sc, Student &st) {
	cout << "姓名:" << st.name << "  学号:" << st.number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	show(sc, st);

	return 0;
}

2、将成员函数声明为友元函数

一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。

#include <iostream>
#include <string>
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	void show(Score &sc);
};

class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void Student::show(Score &sc);
};

void Student::show(Score &sc) {
	cout << "姓名:" << name << "  学号:" << number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	st.show(sc);

	return 0;
}

说明:

        一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;

3.10.2 友元类

可以将一个类声明为另一个类的友元

class Y{
    ···
};
class X{
    friend Y;    //声明类Y为类X的友元类
};

当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。

友元关系不具有交换性传递性

3.11 类的组合

在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象

class Y{
    ···
};
class X{
    Y y;
    ···
};
3.12 共享数据的保护

常类型的引入就是为了既保护数据共享又防止数据被改动。常类型是指使用类型修饰符const说明的类型,常类型的变量或对象成员的值在程序运行期间是不可改变的。

3.12.1 常引用

如果在说明引用时用const修饰,则被说明的引用为常引用。常引用所引用的对象不能被更新。如果用常引用做形参,便不会产生对实参的不希望的更改。

const 类型& 引用名

int a = 5;
const int& b = a;
此时再对b赋值是非法的。
---------------------------
int add(const int& m, const int& n) {
    return m + n;
}
在此函数中对变量m和变量n更新是非法的
 3.12.2 常对象

如果在说明对象时用const修饰,则被说明的对象为常对象。常对象中的数据成员为常量且必须要有初值。

类名 const 对象名[(参数表)];

const Date date(2021, 5, 31);
 3.12.3 常对象成员

1、常数据成员

const 类型 数据成员的名;

类的数据成员可以是常量或常引用,使用const说明的数据成员称为常数据成员。如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。

class Date {
private:
    const int year;
    const int month;
    const int day;
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {}
};

一旦某对象的常数据成员初始化后,该数据成员的值是不能改变的。

2、常成员函数

类型 函数名(参数表) const;

const是函数类型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。

class Date{
private:
	int year;
	int month;
	int day;
public:
	Date(int y, int m, int d) : year(y), month(m), day(d){

	}
	void showDate();
	void showDate() const;
};

void Date::showDate() {
	//···
}

void Date::showDate() const {
	//···
}

关键字const可以被用于对重载函数进行区分。

说明:

  1. 常成员函数可以访问常数据成员,也可以访问普通数据成员。
  2. 常对象只能调用它的常成员对象,而不能调用普通成员函数。常成员函数是常对象唯一的对外接口。
  3. 常对象函数不能更新对象的数据成员,也不能调用该类的普通成员函数,这就保证了在常成员函数中绝不会更新数据成员的值。

 四、继承与派生

继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类父类,在它基础上建立的新类称为派生类子类

4.1 继承与派生的概念

类的继承是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。类的继承和派生机制较好地解决了代码重用的问题。

关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。

使用继承的案例如下:

#include <iostream>
#include <string>
using namespace std;

class Person{
private:
	string name;
	string id_number;
	int age;
public:
	Person(string name1, string id_number1, int age1) {
		name = name1;
		id_number = id_number1;
		age = age1;
	}
	~Person() {

	}
	void show() {
		cout << "姓名: " << name << "  身份证号: " << id_number << " 年龄: " << age << endl;
	}
};

class Student:public Person{
private:
	int credit;
public:
	Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
		credit = credit1;
	}
	~Student() {

	}
	void show() {
		Person::show();
		cout << "学分: " << credit << endl;
	}
};

int main() {
	Student stu("白", "110103**********23", 12, 123);
	stu.show();

	return 0;
}

从已有类派生出新类时,可以在派生类内完成以下几种功能:

  1. 可以增加新的数据成员和成员函数
  2. 可以对基类的成员进行重定义
  3. 可以改变基类成员在派生类中的访问属性

基类成员在派生类中的访问属性

 派生类可以继承基类中除了构造函数与析构函数之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性也有所不同。

基类中的成员继承方式基类在派生类中的访问属性
privatepublic|protected|private不可直接访问
publicpublic|protected|privatepublic|protected|private
protectedpublic|protected|privateprotected|protected|private
  1. private 继承:当使用 private 继承时,基类的公有成员和保护成员在派生类中都将变为私有成员,基类的私有成员在派生类中不可访问。
  2. public 继承:当使用 public 继承时,基类的公有成员在派生类中仍然保持为公有成员,基类的保护成员在派生类中仍然保持为保护成员,基类的私有成员在派生类中不可访问。
  3. protected 继承:当使用 protected 继承时,基类的公有成员和保护成员在派生类中都将变为保护成员,基类的私有成员在派生类中不可访问。

派生类对基类成员的访问规则 

基类的成员可以有public、protected、private3中访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。

派生类对基类成员的访问形式主要有以下两种:

  • 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
  • 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。


 

4.2 派生类的构造函数和析构函数

构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。

调用顺序

#include <iostream>
#include <string>
using namespace std;

class A{
public:
	A() {
		cout << "A类对象构造中..." << endl;
	}
	~A() {
		cout << "析构A类对象..." << endl;
	}
};

class B : public A{
public:
	B() {
		cout << "B类对象构造中..." << endl;
	}
	~B(){
		cout << "析构B类对象..." << endl;
	}
};

int main() {
	B b;
	return 0;
}

代码运行结果如下:

A类对象构造中...
B类对象构造中...
析构B类对象...
析构A类对象...

可见:构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。

派生类构造函数和析构函数的构造规则

#include <iostream>
#include <string>
using namespace std;

// 基类 Person
class Person {
private:
    string name;
    int age;
public:
    Person(const string& n, int a) : name(n), age(a) {
        cout << "Person constructor called." << endl;
    }
    ~Person() {
        cout << "Person destructor called." << endl;
    }
    void display() const {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

// 派生类 Student
class Student : public Person {
private:
    string id;
public:
    Student(const string& n, int a, const string& i) : Person(n, a), id(i) {
        cout << "Student constructor called." << endl;
    }
    ~Student() {
        cout << "Student destructor called." << endl;
    }
    void display() const {
        Person::display();
        cout << "ID: " << id << endl;
    }
};

int main() {
    Student stu("Alice", 20, "20220001");
    cout << "Student info:" << endl;
    stu.display();
    cout << "-------------------" << endl;
    return 0;
}
派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
    派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
    派生类新增成员的初始化语句
}

在定义派生类对象时,构造函数的调用顺序如下:

  • 调用基类的构造函数,对基类数据成员初始化。
  • 调用子对象的构造函数,对子对象的数据成员初始化。
  • 调用派生类的构造函数体,对派生类的数据成员初始化。

说明:

  1. 当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
  2. 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
  3. 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。

4.3 调整基类成员在派生类中的访问属性的其他方法

 派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“::”,即

基类名::成员名

访问声明

访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。

class B:private A{
private:
    int y;
public:
    B(int x1, int y1) : A(x1) {
        y = y1;
    }
    A::show;               //访问声明
};

访问声明在使用时应注意以下几点:

  1. 数据成员也可以使用访问声明。
  2. 访问声明中只含不带类型和参数的函数名或变量名。
  3. 访问声明不能改变成员在基类中的访问属性。
  4. 对于基类的重载函数名,访问声明将对基类中所有同名函数其起作用。

4.4 多继承 

声明多继承派生类的一般形式如下:

class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n {
    派生类新增的数据成员和成员函数
};
默认的继承方式是private

多继承派生类的构造函数与析构函数:

与单继承派生类构造函数相同,多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。

多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。

 4.5 虚基类

虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。

#include <iostream>
#include <string>
using namespace std;

class Base{
protected:
	int a;
public:
	Base(){
		a = 5;
		cout << "Base a = " << a << endl;
	}
};

class Base1: public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2: public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};

class Derived: public Base1, public Base2{
public:
	Derived() {
		cout << "Base1::a = " << Base1::a << endl;
		cout << "Base2::a = " << Base2::a << endl;
	}
};

int main() {
	Derived obj;
	return 0;
}

代码执行结果如下

Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25

虚基类的声明:

不难理解,如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字virtual将base声明为虚基类。

声明虚基类的语法形式如下:

class 派生类:virtual 继承方式 类名{
    ·····
};

上述代码修改如下:

class Base1:virtual public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2:virtual public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};

运行结果如下:

Base a = 5
Base1 a = 15
Base2 a = 35
Base1::a = 35
Base2::a = 35

虚基类的初始化:

虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:

  1. 如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
  2. 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
  3. 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
  4. 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
  5. 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。

4.6 赋值兼容规则 

在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用子类的对象代替。

例如,下面声明的两个类:

class Base{
    ·····
};
class Derived: public Base{
    ·····
};

根据赋值兼容规则,在基类Base的对象可以使用的任何地方,都可以使用派生类Derived的对象来代替,但只能使用从基类继承来的成员。具体的表现在以下几个方面:

  • 派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员。
Base b;
Derived d;
b = d;
  • 派生类对象可以初始化基类对象的引用。
Derived d;
Base &br = d;
  • 派生类对象的地址可以赋值给指向基类对象的指针。
Derived d;
Base *bp = &d;

五、多态性与虚函数

        多态性是面向对象程序设计的重要特征之一。多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。

5.1 多态性概述

        所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”。

        从实现的角度来讲,多态可以划分为两类:编译时的多态和运行时的多态

        在C++中,多态的实现和连编这一概念有关。所谓连编就是把函数名与函数体的程序代码连接在一起的过程。

        静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。

        运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。

        一般而言,编译型语言(如C,Pascal)采用静态连编,而解释型语言(如LISP)采用动态连编。静态连编要求在程序编译时就知道调用函数的全部信息。因此,这种连编类型的函数调用速度快、效率高,但缺乏灵活性;而动态连编方式恰好相反,采用这种连编方式,一直要到程序运行时才能确定调用哪个函数,它降低了程序的运行效率,但增强了程序的灵活性。纯粹的面向对象程序语言由于其执行机制是消息传递,所以只能采用动态连编。C++实际上采用了静态连编和动态连编相结合的方式。

        在C++中,编译时多态性主要是通过函数重载和运算符重载实现的运行时多态性主要是通过虚函数来实现的

5.2 虚函数

虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:

virtual 返回类型 函数名(形参表) {
    函数体
}

在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

#include <iostream>
#include <string>
using namespace std;

class Family{
private:
	string flower;
public:
	Family(string name = "鲜花"): flower(name) { }
	string getName() {
		return flower;
	}
	virtual void like() {
		cout << "家人喜欢不同的花: " << endl;
	}
};

class Mother: public Family{
public:
	Mother(string name = "月季"): Family(name) { }
	void like() {
		cout << "妈妈喜欢" << getName() << endl;
	}
};

class Daughter: public Family{
public:
	Daughter(string name = "百合"): Family(name) { }
	void like() {
		cout << "女儿喜欢" << getName() << endl;
	}
};

int main() {
	Family *p;
	Family f;
	Mother mom;
	Daughter dau;
	p = &f;
	p->like();
	p = &mom;
	p->like();
	p = &dau;
	p->like();

	return 0;
}

 程序运行结果如下:

家人喜欢不同的花:
妈妈喜欢月季
女儿喜欢百合

C++规定,如果在派生类中,没有用virtual显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。

下面对虚函数的定义做几点说明:

  • 由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是派生类从其基类公有派生。因此,通过定义虚函数来使用多态性机制时,派生类必须从它的基类公有派生。
  • 必须首先在基类中定义虚函数;
  • 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
  • 虽然使用对象名和点运算符的方式也可以调用虚函数,如mom.like()可以调用虚函数Mother::like()。但是,这种调用是在编译时进行的静态连编,它没有充分利用虚函数的特性,只有通过基类指针访问虚函数时才能获得运行时的多态性
  • 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
  • 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  • 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
  • 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。

在一个派生类中重新定义基类的虚函数是函数重载的另一种形式。

多继承可以视为多个单继承的组合,因此,多继承情况下的虚函数调用与单继承下的虚函数调用由相似之处。

5.3 虚析构函数

如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

Base *p;
p = new Derived;
delete p;
-----------------
输出:调用基类Base的析构函数

原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。

如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。一般格式为

virtual ~类名(){
    ·····
}

虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数。示例如下,

#include <iostream>
#include <string>
using namespace std;

class Base{
public:
	virtual ~Base() {
		cout << "调用基类Base的析构函数..." << endl;
	}
};

class Derived: public Base{
public:
	~Derived() {
		cout << "调用派生类Derived的析构函数..." << endl;
	}
};

int main() {
	Base *p;
	p = new Derived;
	delete p;
	return 0;
}

输出如下:

调用派生类Derived的析构函数...
调用基类Base的析构函数...
5.4 纯虚函数

纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

5.5 抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:

  • 由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
  • 不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
  • 抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。
  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
  • 如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
 5.6 示例:利用多态计算面积

应用C++的多态性,计算三角形、矩形和圆的面积。

#include <iostream>
using namespace std;

/*** 定义一个公共基类 ***/
class Figure{
protected:
	double x, y;
public:
	Figure(double a, double b): x(a), y(b) {  }
	virtual void getArea()      //虚函数
	{  
		cout << "No area computation defind for this class.\n";
	}
};

class Triangle: public Figure{
public:
	Triangle(double a, double b): Figure(a, b){  }
	//虚函数重定义,用于求三角形的面积
	void getArea(){
		cout << "Triangle with height " << x << " and base " << y;
		cout << " has an area of " << x * y * 0.5 << endl;
	}
};

class Square: public Figure{
public:
	Square(double a, double b): Figure(a, b){  }
	//虚函数重定义,用于求矩形的面积
	void getArea(){
		cout << "Square with dimension " << x << " and " << y;
		cout << " has an area of " << x * y << endl;
	}
};

class Circle: public Figure{
public:
	Circle(double a): Figure(a, a){  }
	//虚函数重定义,用于求圆的面积
	void getArea(){
		cout << "Circle with radius " << x ;
		cout << " has an area of " << x * x * 3.14 << endl;
	}
};

int main(){
	Figure *p;
	Triangle t(10.0, 6.0);
	Square s(10.0, 6.0);
	Circle c(10.0);

	p = &t;
	p->getArea();
	p = &s;
	p->getArea();
	p = &c;
	p->getArea();

	return 0;
}

程序输出如下:

Triangle with height 10 and base 6 has an area of 30
Square with dimension 10 and 6 has an area of 60
Circle with radius 10 has an area of 314

六、运算符重载

6.1 运算符重载概述

可以重定义或重载大部分 C++ 内置的运算符。这样,就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

Box operator+(const Box&);

声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下所示:

Box operator+(const Box&, const Box&);
6.2 +号运算符重载

下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用 this 运算符进行访问,如下所示:

#include <iostream>
using namespace std;
 
class Box
{
   public:
 
      double getVolume(void)
      {
         return length * breadth * height;
      }
      void setLength( double len )
      {
          length = len;
      }
 
      void setBreadth( double bre )
      {
          breadth = bre;
      }
 
      void setHeight( double hei )
      {
          height = hei;
      }
      // 重载 + 运算符,用于把两个 Box 对象相加
      Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
// 程序的主函数
int main( )
{
   Box Box1;                // 声明 Box1,类型为 Box
   Box Box2;                // 声明 Box2,类型为 Box
   Box Box3;                // 声明 Box3,类型为 Box
   double volume = 0.0;     // 把体积存储在该变量中
 
   // Box1 详述
   Box1.setLength(6.0); 
   Box1.setBreadth(7.0); 
   Box1.setHeight(5.0);
 
   // Box2 详述
   Box2.setLength(12.0); 
   Box2.setBreadth(13.0); 
   Box2.setHeight(10.0);
 
   // Box1 的体积
   volume = Box1.getVolume();
   cout << "Volume of Box1 : " << volume <<endl;
 
   // Box2 的体积
   volume = Box2.getVolume();
   cout << "Volume of Box2 : " << volume <<endl;
 
   // 把两个对象相加,得到 Box3
   Box3 = Box1 + Box2;
 
   // Box3 的体积
   volume = Box3.getVolume();
   cout << "Volume of Box3 : " << volume <<endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Volume of Box1 : 210
Volume of Box2 : 1560
Volume of Box3 : 5400
6.3 可重载运算符/不可重载运算符

下面是可重载的运算符列表:

双目算术运算符+ (加),-(减),*(乘),/(除),% (取模)
关系运算符==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
逻辑运算符||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符+ (正),-(负),*(指针),&(取地址)
自增自减运算符++(自增),--(自减)
位运算符| (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符=, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
空间申请与释放new, delete, new[ ] , delete[]
其他运算符()(函数调用),->(成员访问),,(逗号),[](下标)

下面是不可重载的运算符列表:

  • .:成员访问运算符
  • .*, ->*:成员指针访问运算符
  • :::域运算符
  • sizeof:长度运算符
  • ?::条件运算符
  • #: 预处理符号

七、函数模板与类模板

        利用模板机制可以显著减少冗余信息,能大幅度地节约程序代码,进一步提高面向对象程序的可重用性和可维护性。模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现代码的重用,使得一段程序可以用于处理多种不同类型的对象,大幅度地提高程序设计的效率。

7.1 模板的概念

在程序设计中往往存在这样的现象:两个或多个函数的函数体完全相同,差别仅在与它们的参数类型不同。

例如:

int Max(int x, int y) {
    return x >= y ? x : y;
}

double Max(double x, double y) {
    return x >= y ? x : y;
}

能否为上述这些函数只写出一套代码呢?解决这个问题的一种方式是使用宏定义

#define Max(x, y)((x >= y) ? x : y)

宏定义带来的另一个问题是,可能在不该替换的地方进行了替换,而造成错误。事实上,由于宏定义会造成不少麻烦,所以在C++中不主张使用宏定义。解决以上问题的另一个方法就是使用模板。

7.2 函数模板

所谓函数模板,实际上是建立一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。在调用函数时,系统会根据实参的类型(模板实参)来取代模板中的虚拟类型,从而实现不同函数的功能。

函数的声明格式如下:

template <typename 类型参数>
返回类型 函数名(模板形参表)
{
    函数体
}
也可以定义为如下形式
template <class 类型参数>
返回类型 函数名(模板形参表)
{
    函数体
}

 实际上,template是一个声明模板的关键字,它表示声明一个模板。类型参数(通常用C++标识符表示,如T、type等)实际上是一个虚拟的类型名,使用前并未指定它是哪一种具体的类型,但使用函数模板时,必须将类型实例化。类型参数前需加关键字typename或class,typename和class的作用相同,都是表示一个虚拟的类型名(即类型参数)。

例1:一个与指针有关的函数模板

#include <iostream>
using namespace std;

template <typename T>
T Max(T *array, int size = 0) {
	T max = array[0];
	for (int i = 1	; i < size; i++) {
		if (array[i] > max) max = array[i];
	}
	return max;
}

int main() {
	int array_int[] = {783, 78, 234, 34, 90, 1};
	double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
	int imax = Max(array_int, 6);
	double dmax = Max(array_double, 6);
	cout << "整型数组的最大值是:" << imax << endl;
	cout << "双精度型数组的最大值是:" << dmax << endl;
	return 0;
}

例2:函数模板的重载

#include <iostream>
using namespace std;

template <class Type>
Type Max(Type x, Type y) {
	return x > y ? x : y;
}

template <class Type>
Type Max(Type x, Type y, Type z) {
	Type t = x > y ? x : y;
	t = t > z ? t : z;
	return t;
}

int main() {
	cout << "33,66中最大值为 " << Max(33, 66) << endl;
	cout << "33,66,44中最大值为 " << Max(33, 66, 44) << endl;
	return 0;
}

注意:

  • 在函数模板中允许使用多个类型参数。但是,应当注意template定义部分的每个类型参数前必须有关键字typename或class。
  • 在template语句与函数模板定义语句之间不允许插入别的语句。
  • 同一般函数一样,函数模板也可以重载。
  • 函数模板与同名的非模板函数可以重载。在这种情况下,调用的顺序是:首先寻找一个参数完全匹配的非模板函数,如果找到了就调用它;若没有找到,则寻找函数模板,将其实例化,产生一个匹配的模板参数,若找到了,就调用它。
7.3 类模板

所谓类模板,实际上就是建立一个通用类,其数据成员、成员函数的返回类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会根据实参的类型来取代类模板中虚拟类型,从而实现不同类的功能。

template <typename T>
class Three{
private:
    T x, y, z;
public:
    Three(T a, T b, T c) {
        x = a; y = b; z = c;
    }
    T sum() {
        return x + y + z;
    }
}

上面的例子中,成员函数(其中含有类型参数)是定义在类体内的。但是,类模板中的成员函数也可以在类模板体外定义。此时,若成员函数中有类型参数存在,则C++有一些特殊的规定:

  • 需要在成员函数定义之前进行模板声明;
  • 在成员函数名前要加上“类名<类型参数>::”;

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

template <typename 类型参数>
函数类型 类名<类型参数>::成员函数名(形参表)
{
    ·····
}

 例如,上例中成员函数sum()在类外定义时,应该写成

template<typename T>
T Three<T>::sum() {
    return x + y + z;
}

例子:栈类模板的使用

#include <iostream>
#include <string>
using namespace std;

const int size = 10;
template <class T>
class Stack{
private:
	T stack[size];
	int top;
public:
	void init() {
		top = 0;
	}
	void push(T t);
	T pop();
};

template <class T>
void Stack<T>::push(T t) {
	if (top == size) {
		cout << "Stack is full!" << endl;
		return;
	}
	stack[top++] = t;
}

template <class T>
T Stack<T>::pop() {
	if (top == 0) {
		cout << "Stack is empty!" <<endl;
		return 0;
	}
	return stack[--top];
}

int main() {
	Stack<string> st;
	st.init();
	st.push("aaa");
	st.push("bbb");
	cout << st.pop() << endl;
	cout << st.pop() << endl;

	return 0;
}


//输出
bbb
aaa

八、C++的输入和输出

8.1 C++为何建立自己的输入/输出系统

C++除了完全支持C语言的输入输出系统外,还定义了一套面向对象的输入/输出系统。C++的输入输出系统比C语言更安全、可靠。

c++的输入/输出系统明显地优于C语言的输入/输出系统。首先,它是类型安全的、可以防止格式控制符与输入输出数据的类型不一致的错误。另外,C++可以通过重载运算符“>>”和"<<",使之能用于用户自定义类型的输入和输出,并且向预定义类型一样有效方便。C++的输入/输出的书写形式也很简单、清晰,这使程序代码具有更好的可读性。

8.2 C++的流库及其基本结构


“流”指的是数据从一个源流到一个目的的抽象,它负责在数据的生产者(源)和数据的消费者(目的)之间建立联系,并管理数据的流动。凡是数据从一个地方传输到另一个地方的操作都是流的操作,从流中提取数据称为输入操作(通常又称提取操作),向流中添加数据称为输出操作(通常又称插入操作)。

C++的输入/输出是以字节流的形式实现的。在输入操作中,字节流从输入设备(如键盘、磁盘、网络连接等)流向内存;在输出操作中,字节流从内存流向输出设备(如显示器、打印机、网络连接等)。字节流可以是ASCII码、二进制形式的数据、图形/图像、音频/视频等信息。文件和字符串也可以看成有序的字节流,分别称为文件流和字符串流。

用于输入/输出的头文件

C++编译系统提供了用于输入/输出的I/O类流库。I/O流类库提供了数百种输入/输出功能,I/O流类库中各种类的声明被放在相应的头文件中,用户在程序中用#include命令包含了有关的头文件就相当于在本程序中声明了所需要用到的类。常用的头文件有:

  • iostream包含了对输入/输出流进行操作所需的基本信息。使用cin、cout等流对象进行针对标准设备的I/O操作时,须包含此头文件。
  • fstream用于用户管理文件的I/O操作。使用文件流对象进行针对磁盘文件的操作,须包含此头文件。
  • strstream用于字符串流的I/O操作。使用字符串流对象进行针对内存字符串空间的I/O操作,须包含此头文件。
  • iomanip用于输入/输出的格式控制。在使用setw、fixed等大多数操作符进行格式控制时,须包含此头文件。

 用于输入/输出的流类

I/O流类库中包含了许多用于输入/输出操作的类。其中,类istream支持流输入操作,类ostream支持流输出操作,类iostream同时支持流输入和输出操作。

下表列出了iostream流类库中常用的流类,以及指出了这些流类在哪个头文件中声明。

类名类名说明头文件
抽象流基类ios流基类iostream
输入流类istream通用输入流类和其他输入流的基类iostream
输入流类ifstream输入文件流类fstream
输入流类istrstream输入字符串流类strstream
输出流类ostream通用输出流类和其他输出流的基类iostream
输出流类ofstream输出文件流类fstream
输出流类ostrstream输出字符串流类strstream
输入/输出流类iostream通用输入输出流类和其他输入/输出流的基类iostream
输入/输出流类fstream输入/输出文件流类fstream
输入/输出流类strstream输入/输出字符串流类strstream

8.3 预定义的流对象

用流定义的对象称为流对象。与输入设备(如键盘)相关联的流对象称为输入流对象;与输出设备(如屏幕)相联系的流对象称为输出流对象。

C++中包含几个预定义的流对象,它们是标准输入流对象cin、标准输出流对象cout、非缓冲型的标准出错流对象cerr和缓冲型的标准出错流对象clog

8.4 输入/输出流的成员函数

使用istream和类ostream流对象的一些成员函数,实现字符的输出和输入。

1、put()函数
    cout.put(单字符/字符形变量/ASCII码);
2、get()函数
    get()函数在读入数据时可包括空白符,而提取运算符“>>”在默认情况下拒绝接收空白字符。
    cin.get(字符型变量)
3、getline()函数
    cin.getline(字符数组, 字符个数n, 终止标志字符)
    cin.getline(字符指针, 字符个数n, 终止标志字符)
4、ignore()函数
    cin.ignore(n, 终止字符)
    ignore()函数的功能是跳过输入流中n个字符(默认个数为1),或在遇到指定的终止字符(默认终止字符是EOF)时提前结束。
8.5 预定义类型输入/输出的格式控制

在很多情况下,需要对预定义类型(如int、float、double型等)的数据的输入/输出格式进行控制。在C++中,仍然可以使用C中的printf()和scanf()函数进行格式化。除此之外,C++还提供了两种进行格式控制的方法:一种是使用ios类中有关格式控制的流成员函数进行格式控制;另一种是使用称为操作符的特殊类型的函数进行格式控制。

1、用流成员函数进行输入/输出格式控制

  • 设置状态标志的流成员函数setf()
  • 清除状态标志的流成员函数unsetf()
  • 设置域宽的流成员函数width()
  • 设置实数的精度流成员函数precision()
  • 填充字符的流成员函数fill()

2、使用预定义的操作符进行输入/输出格式控制

3、使用用户自定义的操作符进行输入/输出格式控制

若为输出流定义操作符函数,则定义形式如下:
ostream &操作符名(ostream &stream)
{
    自定义代码
    return stream;
}

若为输入流定义操作符函数,则定义形式如下:
istream &操作符名(istream &stream)
{
    自定义代码
    return stream;
}

例如,

#include <iostream>
#include <iomanip>
using namespace std;

ostream &output(ostream &stream)
{
	stream.setf(ios::left);
	stream << setw(10) << hex << setfill('-');
	return stream;
}
// 这个函数接受一个 ostream 类型的参数 stream,并在该流上设置输出格式。首先使用 stream.setf(ios::left) 来设置左对齐,然后使用 setw(10) 设置输出宽度为10,hex 表示后续的数字输出将以十六进制形式显示,setfill('-') 用来设置填充字符为 '-'。最后返回流对象。

int main() {
	cout << 123 << endl;
	cout << output << 123 << endl;    // 使用自定义输出流操作符output来输出123
	return 0;
}

输出结果如下:

123
7b--------
8.6 文件的输入/输出

所谓文件,一般指存放在外部介质上的数据的集合。

文件流是以外存文件为输入/输出对象的数据流。输出文件流是从内存流向外存文件的数据,输入文件流是从外存流向内存的数据。

根据文件中数据的组织形式,文件可分为两类:文本文件和二进制文件。

在C++中进行文件操作的一般步骤如下:

  • 为要进行操作的文件定义一个流对象。
  • 建立(或打开)文件。如果文件不存在,则建立该文件。如果磁盘上已存在该文件,则打开它。
  • 进行读写操作。在建立(或打开)的文件基础上执行所要求的输入/输出操作。
  • 关闭文件。当完成输入/输出操作时,应把已打开的文件关闭。
8.7 文件的打开与关闭 

为了执行文件的输入/输出,C++提供了3个文件流类。

类名说明功能
istream输入文件流类用于文件的输入
ofstream输出文件流类用于文件的输出
fstream输入/输出文件流类用于文件的输入/输出

这3个文件流类都定义在头文件fstream中。

要执行文件的输入/输出,须完成以下几件工作:

  1. 在程序中包含头文件fstream
  2. 建立流对象
  3. 使用成员函数open()打开文件。
  4. 进行读写操作。
  5. 使用close()函数将打开的文件关闭。

8.8 文本文件的读/写 

例子:把字符串“I am a student.”写入磁盘文件text.txt中。

#include <iostream>
#include <fstream>
using namespace std;

int main() {
	ofstream fout("../test.txt", ios::out);
	if (!fout) {
		cout << "Cannot open output file." << endl;
		exit(1);
	}
	fout << "I am a student.";
	fout.close();

	return 0;
}

//在 main 函数中,首先创建了一个 ofstream 对象 fout,并打开一个
//名为 "test.txt" 的文件用于写入(ios::out 表示输出模式,即写入文件)。
//如果该文件不存在,则会创建一个新文件;如果文件已存在,则会清空其内容。

 例子: 把磁盘文件test1.dat中的内容读出并显示在屏幕上。

#include <iostream>
#include <fstream>
using namespace std;

int main() {
	ifstream fin("../test.txt", ios::in);
	if (!fin) {
		cout << "Cannot open output file." << endl;
		exit(1);
	}
	char str[80];
	fin.getline(str , 80);
	cout << str <<endl;
	fin.close();

	return 0;
}
8.9 二进制文件的读写

用get()函数和put()函数读/写二进制文件

例子:将a~z的26个英文字母写入文件,而后从该文件中读出并显示出来。

#include <iostream>
#include <fstream>
using namespace std;

void writeToFile(const string& filename) {
    ofstream outfile(filename, ios::binary); // 打开文件用于写入,二进制模式
    if (!outfile) {
        cerr << "Cannot open output file." << endl;
        exit(1);
    }

    char ch = 'a';
    for (int i = 0; i < 26; ++i) {
        outfile.put(ch); // 写入字符到文件中
        ++ch;
    }

    outfile.close(); // 关闭文件
}

void readFromFile(const string& filename) {
    ifstream infile(filename, ios::binary); // 打开文件用于读取,二进制模式
    if (!infile) {
        cerr << "Cannot open input file." << endl;
        exit(1);
    }

    char ch;
    while (infile.get(ch)) { // 从文件中读取字符
        cout << ch; // 输出字符到控制台
    }

    infile.close(); // 关闭文件
}

int main() {
    const string filename = "letters.bin"; // 文件名

    // 写入字母到文件
    writeToFile(filename);

    // 从文件中读取并显示字母
    readFromFile(filename);

    return 0;
}

用read()函数和write()函数读写二进制文件

有时需要读写一组数据(如一个结构变量的值),为此C++提供了两个函数read()和write(),用来读写一个数据块,这两个函数最常用的调用格式如下:

inf.read(char *buf, int len);
outf.write(const char *buf, int len);

例子:将两门课程的课程名和成绩以二进制形式存放在磁盘文件中。

#include <iostream>
#include <fstream>
using namespace std;

struct list{
	char course[15];
	int score;
};

int main() {
	list ob[2] = {"Computer", 90, "History", 99};      //创建了一个名为 ob 的结构体数组
	ofstream out("test.txt", ios::binary);
	if (!out) {
		cout << "Cannot open output file.\n";
		abort();   //退出程序,作用与exit相同。
	}
	for (int i = 0; i < 2; i++) {
		out.write((char*) &ob[i], sizeof(ob[i]));
	}       
	out.close();

	return 0;
}

//(char*) &ob[i] 是一个类型转换,将结构体数组 ob 中第 i 个元素的地址转换为 char* 类型。
//&ob[i] 表示结构体变量的地址,(char*) 将该地址转换为指向字符类型的指针,这样可以以字节为单位
//写入数据。sizeof(ob[i]) 返回结构体 ob[i] 的大小,以字节为单位。
//sizeof 是一个 C++ 内置的运算符,用于计算对象或类型的大小。

例子:将上述例子以二进制形式存放在磁盘文件中的数据读入内存。

 

#include <iostream>
#include <fstream>
using namespace std;

struct list{
	char course[15];
	int score;
};

int main() {
	list ob[2];
	ifstream in("test.txt", ios::binary);
	if (!in) {
		cout << "Cannot open input file.\n";
		abort();
	}
	for (int i = 0; i < 2; i++) {
		in.read((char *) &ob[i], sizeof(ob[i]));
		cout << ob[i].course << " " << ob[i].score << endl; 
	}
	in.close();

	return 0;
}

检测文件结束

 在文件结束的地方有一个标志位,即为EOF。采用文件流方式读取文件时,使用成员函数eof()可以检测到这个结束符。如果该函数的返回值非零,表示到达文件尾。返回值为零表示未达到文件尾。该函数的原型是:

int eof();
函数eof()的用法示例如下:
ifstream ifs;
···
if (!ifs.eof())   //尚未到达文件尾
    ···
还有一个检测方法就是检查该流对象是否为零,为零表示文件结束。
ifstream ifs;
···
if (!ifs)
    ···
如下例子:
while (cin.get(ch))
    cut.put(ch);
这是一个很通用的方法,就是检测文件流对象的某些成员函数的返回值是否为0,为0表示该流(亦即对应的文件)到达了末尾。

九、异常处理和命名空间

9.1 异常处理

程序中常见的错位分为两大类:编译时错误运行时错误。编译时的错误主要是语法错误,如关键字拼写错误、语句末尾缺分号、括号不匹配等。运行时出现的错误统称为异常,对异常的处理称为异常处理。

C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。

例子:输入三角形的三条边长,求三角形的面积。当输入边的长度小于0时,或者当三条边都大于0时但不能构成三角形时,分别抛出异常,结束程序运行。

#include <iostream>
#include <cmath>
using namespace std;

double triangle(double a, double b, double c) {
	double s = (a + b + c) / 2;
	if (a + b <= c || a + c <= b || b + c <= a) {
		throw 1.0;        //语句throw抛出double异常
	}
	return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
	double a, b, c;
	try {
		cout << "请输入三角形的三个边长(a, b, c): " << endl;
		cin >> a >> b >> c;
		if (a < 0 || b < 0 || c < 0) {
			throw 1;   //语句throw抛出int异常
		}
		while (a > 0 && b > 0 && c > 0) {
			cout << "a = " << a << " b = " << b << " c = " << c << endl;
			cout << "三角形的面积 = " << triangle(a, b, c) << endl;
			cin >> a >> b >> c;
			if (a <= 0 || b <= 0 || c <= 0) {
				throw 1;
			}
		}
	} catch (double) {
		cout << "这三条边不能构成三角形..." << endl;
	} catch (int) {
		cout << "边长小于或等于0..." << endl;
	}
	return 0;
}
9.2 命名空间和头文件命名规则

命名空间:一个由程序设计者命名的内存区域。程序设计者可以根据需要指定一些有名字的命名空间,将各命名空间中声明的标识符与该命名空间标识符建立关联,保证不同命名空间的同名标识符不发生冲突。

1.带扩展名的头文件的使用
在C语言程序中头文件包括扩展名.h,使用规则如下面例子
    #include <stdio.h>
2.不带扩展名的头文件的使用
C++标准要求系统提供的头文件不包括扩展名.h,如string,string.h等。
    #include <cstring>

十、STL标准模板库

标准模板库Standard Template Library)中包含了很多实用的组件,利用这些组件,程序员编程方便而高效。

10.1 Vector

vector容器与数组类似,包含一组地址连续的存储单元。对vector容器可以进行很多操作,包括查询、插入、删除等常见操作。

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> nums;    //定义了一个名为 nums 的整型向量
	nums.insert(nums.begin(), 99);    //将整数 99 插入到向量的开头。
	nums.insert(nums.begin(), 34);    //将整数 34 插入到向量的开头。
	nums.insert(nums.end(), 1000);    //将整数 1000 插入到向量的末尾。
	nums.push_back(669);              //将整数 669 添加到向量的末尾。

	cout << "\n当前nums中元素为: " << endl;
	for (int i = 0; i < nums.size(); i++)    //使用循环遍历输出向量中的所有元素。
		cout << nums[i] << " " ;

	cout << nums.at(2);    //使用 at() 函数访问向量中的第 3 个元素,并将其输出。
	nums.erase(nums.begin());    // 删除向量开头的元素。
	nums.pop_back();             //删除向量末尾的元素。

	cout << "\n当前nums中元素为: " << endl;
	for (int i = 0; i < nums.size(); i++)    //再次使用循环遍历输出向量中的所有元素,以展示删除操作的效果。
		cout << nums[i] << " " ;

	return 0;
}


输出

当前nums中元素为:  
34 99 1000 669 1000
当前nums中元素为:  
99 1000
10.2 list容器
#include <iostream>
#include <list>
using namespace std;

int main() {
	list<int> number;    //创建了一个空的整型链表 number,list 并不保证元素在内存中的连续存储,而是通过指针连接的方式进行存储,因此它是一个双向链表
	list<int>::iterator niter;     //niter 是一个名为 niter 的变量,它的类型是 list<int>::iterator,表示 list<int> 类型的链表的迭代器。通过 niter,我们可以在链表中移动,并访问当前位置的元素
	number.push_back(123);
	number.push_back(234);
	number.push_back(345);    //向链表末尾插入三个整数:123、234 和 345。

	cout << "链表内容:" << endl;
	for (niter = number.begin(); niter != number.end(); ++niter)
		cout << *niter << endl;
	number.reverse();
	cout << "逆转后的链表内容:" << endl;
	for (niter = number.begin(); niter != number.end(); ++niter)
		cout << *niter << endl;
	number.reverse();

	return 0;
}

10.3 stack

例子:利用栈进行进制转换

#include <iostream>
#include <stack>
using namespace std;

int main() {
	stack<int> st;    //创建了一个整型栈 st,用于存储八进制数的每一位数字
	int num = 100;
	cout << "100的八进制表示为:";
	while (num) {
		st.push(num % 8);    //计算 num 对 8 取余的结果,并将余数压入栈中
		num /= 8;    //将 num 除以 8,以便在下一次迭代中继续处理剩余的商,直到 num 为 0
	}
	int t;
	while (!st.empty()) {
		t = st.top();
		cout << t;
		st.pop();
	}
	cout << endl;

	return 0;
}
10.4 queue
#include <iostream>
#include <queue>
using namespace std;

int main() {
	queue<int> qu;    //创建了一个整型队列 qu
	for (int i = 0; i < 10; i++) 
		qu.push(i * 3 + i);    //每次计算出一个整数并将其插入到队列的尾部
	while (!qu.empty()) {
		cout << qu.front() << " ";
		qu.pop();    //通过 qu.front() 获取队列头部的元素的值,并将其输出,然后调用 qu.pop() 弹出队列头部的元素
	}
	cout << endl;

	return 0;
}
10.5 优先队列priority_queue
#include <iostream>
#include <queue>
#include <functional>
#include <cstdlib>
#include <ctime>
using namespace std;

int main() {
	priority_queue<int> pq;    //创建了一个整型的优先队列 pq
	srand((unsigned)time(0));    //使用当前时间作为随机数种子,以确保每次运行程序时生成的随机数序列都不同
	for (int i = 0; i < 6; i++) {
		int t = rand();
		cout << t << endl;
		pq.push(t);
	}
	cout << "优先队列的值:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << pq.top() << endl;
		pq.pop();    //通过 pq.top() 获取优先队列中优先级最高的元素的值,并将其输出,然后调用 pq.pop() 弹出优先队列中优先级最高的元素
	}

	return 0;
}
10.6 双端队列deque
push_back(): 将元素插入容器的尾部(在向量和双端队列中)。
push_front(): 将元素插入容器的头部(在双端队列和链表中)。
insert(): 在指定位置插入元素。
pop_back(): 移除容器的最后一个元素(在向量和双端队列中)。
pop_front(): 移除容器的第一个元素(在双端队列和链表中)。
erase(): 移除指定位置的元素或指定范围内的元素。
begin(): 返回指向容器第一个元素的迭代器。
end(): 返回指向容器末尾(最后一个元素的下一个位置)的迭代器。
rbegin(): 返回指向容器最后一个元素的逆向迭代器(反向迭代器)。
rend(): 返回指向容器起始位置(第一个元素之前的位置)的逆向迭代器。
size(): 返回容器中元素的个数。
maxsize(): 返回容器的最大可容纳元素数量。
10.7 set
#include <iostream>
#include <set>
#include <string>
using namespace std;

int main() {
	set<string> s;    //创建了一个字符串集合 s
	s.insert("aaa");
	s.insert("bbb");
	s.insert("ccc");    //使用了 insert() 函数将字符串 "aaa"、"bbb" 和 "ccc" 插入到集合中。由于集合是有序的,所以插入的元素会按照字典序进行排序
	if (s.count("aaa") != 0) {
		cout << "存在元素aaa" << endl;
	}
	set<string>::iterator iter;
	for (iter = s.begin(); iter != s.end(); ++iter)     //使用了迭代器 iter 遍历集合中的所有元素,并将其输出到控制台。首先,通过 s.begin() 获取指向集合开头的迭代器,然后使用循环遍历迭代器,直到迭代器指向集合末尾(s.end())。在每次迭代中,通过 *iter 获取迭代器所指向的元素,并将其输出
		cout << *iter << endl;

	return 0;
}
10.8 map
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
	map<string, int> m;    //创建了一个字符串到整数的映射 m
	m["aaa"] = 111;
	m["bbb"] = 222;
	m["ccc"] = 333;    //使用了 [] 运算符为映射中的键赋值,将字符串 "aaa"、"bbb" 和 "ccc" 分别与整数 111、222 和 333 建立映射关系
	if (m.count("aaa")) {
		cout << "键aaa对应的值为" << m.at("aaa") << endl;    
		cout << "键aaa对应的值为" << m["aaa"] << endl;    //通过 m.at("aaa") 或 m["aaa"] 获取键为 "aaa" 对应的值,并将其输出
	}

	return 0;
}

参考资料:《C++面向对象程序设计》✍千处细节、万字总结(建议收藏)_c++面向对象程序设计千处细节-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1469136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Spring MVC】处理器映射器:AbstractHandlerMethodMapping源码分析

目录 一、继承体系 二、HandlerMapping 三、AbstractHandlerMapping 四、AbstractHandlerMethodMapping 4.1 成员属性 4.1.1 MappingRegistry内部类 4.2 AbstractHandlerMethodMapping的初始化 4.3 getHandlerInternal()方法&#xff1a;根据当前的请求url&#xff0c;…

RocketMQ-架构与设计

RocketMQ架构与设计 一、简介二、框架概述1.设计特点 三、架构图1.Producer2.Consumer3.NameServer4.BrokerServer 四、基本特性1.消息顺序性1.1 全局顺序1.2 分区顺序 2.消息回溯3.消息重投4.消息重试5.延迟队列&#xff08;定时消息&#xff09;6.重试队列7.死信队列8.消息语…

智慧城市与数字孪生:共创未来城市新篇章

一、引言 随着科技的飞速发展&#xff0c;智慧城市与数字孪生已成为现代城市建设的核心议题。智慧城市注重利用先进的信息通信技术&#xff0c;提升城市治理水平&#xff0c;改善市民生活品质。而数字孪生则通过建立物理城市与数字模型之间的连接&#xff0c;为城市管理、规划…

接近于pi的程序

在一个平静的午后&#xff0c;两个神秘的数字悄然相遇了。它们分别是-1031158223和-328227871。这两个数字看起来普普通通&#xff0c;但谁知它们背后隐藏着一段令人惊叹的奇幻之旅。 这两个数字其实是π的两位探险家&#xff0c;它们决定通过一次除法运算来探索π的奥秘。它们…

浅谈密码学

文章目录 每日一句正能量前言什么是密码学对称加密简述加密语法Kerckhoffs原则常用的加密算法现代密码学的原则威胁模型&#xff08;按强度增加的顺序&#xff09; 密码学的应用领域后记 每日一句正能量 人生在世&#xff0c;谁也不能做到让任何人都喜欢&#xff0c;所以没必要…

Vue+SpringBoot打造快递管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 数据中心模块2.2 快递类型模块2.3 快递区域模块2.4 快递货架模块2.5 快递档案模块 三、界面展示3.1 登录注册3.2 快递类型3.3 快递区域3.4 快递货架3.5 快递档案3.6 系统基础模块 四、免责说明 一、摘要 1.1 项目介绍 …

【Java程序设计】【C00294】基于Springboot的车辆充电桩管理系统(有论文)

基于Springboot的车辆充电桩管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的车辆充电桩管理系统 本系统前台功能模块分为&#xff1a;首页功能和用户后台管理 后台功能模块分为&#xff1a;管理员功能和…

博途PLC PID仿真(单容水箱液位高度控制含变积分变增益测试)

单容水箱和双荣水箱的微分方程和数值求解,可以参考下面文章链接: https://rxxw-control.blog.csdn.net/article/details/131139432https://rxxw-control.blog.csdn.net/article/details/131139432这篇博客我们利用欧拉求解器在PLC里完成单容水箱的数学建模。PLC也可以和MATL…

Vue模板引用之ref特殊属性

1. 使用实例 <template><input ref"input" name"我是input的name" /><br /><ul><li v-for"arr in array" :key"arr" id"111" ref"itemRefs">{{arr}}</li></ul> </…

我的NPI项目之设备系统启动(八) -- Android14的GKI2.0开发步骤和注意事项

GKI是什么&#xff1f; Google为什么要推行GKI&#xff1f; GKI全称General Kernel Image。GKI在framework和kernel之间提供了标准接口&#xff0c;使得android OS能够轻松适配/维护/兼容不同的设备和linux kernel。 Google引入GKI的目的是将Framework和Kernel进一步的解耦。因…

汇编反外挂

在软件保护领域&#xff0c;尤其是游戏保护中&#xff0c;反外挂是一个重要的议题。外挂通常指的是一种第三方软件&#xff0c;它可以修改游戏数据、操作游戏内存或提供其他作弊功能&#xff0c;从而给玩家带来不公平的优势。为了打击外挂&#xff0c;游戏开发者会采取一系列措…

【数据结构和算法初阶(C语言)】时间复杂度(衡量算法快慢的高端玩家,搭配例题详细剖析)

目录 1.算法效率 1.1如何衡量一个算法的好坏 1.2 算法的复杂度 2.主菜-时间复杂度 2.1 时间复杂度的概念 2.2 大O的渐进表示法 2.2.1算法的最好&#xff0c;最坏和平均的情况 3.经典时间复杂度计算举例 3.1计算冒泡排序的时间复杂度 3.2计算折半查找的时间复杂度 3.…

SQL 中如何实现多表关联查询?

阅读本文之前请参阅----MySQL 数据库安装教程详解&#xff08;linux系统和windows系统&#xff09; 在SQL中&#xff0c;多表关联查询是通过使用JOIN操作来实现的&#xff0c;它允许你从两个或多个表中根据相关列的值来检索数据。以下是几种常见的JOIN类型&#xff1a; …

C语言:指针的进阶讲解

目录 1. 二级指针 1.1 二级指针是什么&#xff1f; 1.2 二级指针的作用 2. 一维数组和二维数组的本质 3. 指针数组 4. 数组指针 5. 函数指针 6. typedef的使用 7. 函数指针数组 7.1 转移表 1. 二级指针 如果了解了一级指针&#xff0c;那二级指针也是可以很好的理解…

(每日持续更新)jdk api之ObjectStreamException基础、应用、实战

博主18年的互联网软件开发经验&#xff0c;从一名程序员小白逐步成为了一名架构师&#xff0c;我想通过平台将经验分享给大家&#xff0c;因此博主每天会在各个大牛网站点赞量超高的博客等寻找该技术栈的资料结合自己的经验&#xff0c;晚上进行用心精简、整理、总结、定稿&…

联想开天昭阳N4620Z笔记本如何恢复出厂麒麟操作系统(图解)

联想开天昭阳N4620Z笔记本简单参数&#xff1a; 中央处理器&#xff1a;KX-6640MA G2 内存&#xff1a;8GB 固态硬盘&#xff1a;512GB SSD 显示器&#xff1a;14.0”FHD 电池&#xff1a;4Cell 操作系统&#xff1a;麒麟KOS中文RTM&#xff08;试用版&#xff09; 此款笔…

[深度学习]yolov9+bytetrack+pyqt5实现目标追踪

【简介】 目标追踪简介 目标追踪是计算机视觉领域中的一个热门研究方向&#xff0c;它涉及到从视频序列中实时地、准确地跟踪目标对象的位置和运动轨迹。随着深度学习技术的快速发展&#xff0c;基于深度学习的目标追踪方法逐渐展现出强大的性能。其中&#xff0c;YOLOv9&…

深入浅出:探究过完备字典矩阵

在数学和信号处理的世界里&#xff0c;我们总是在寻找表达数据的最佳方式。在这篇博文中&#xff0c;我们将探讨一种特殊的矩阵——过完备字典矩阵&#xff0c;这是线性代数和信号处理中一个非常有趣且实用的概念。 什么是过完备字典矩阵&#xff1f; 首先&#xff0c;我们先…

Github 2024-02-24 开源项目日报Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-24统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目5TypeScript项目2C项目1Rust项目1JavaScript项目1HTML项目1Jupyter Notebook项目1 Python - 100天…

【黑马程序员】2、TypeScript介绍_黑马程序员前端TypeScript教程,TypeScript零基础入门到实战全套教程

课程地址&#xff1a;【黑马程序员前端TypeScript教程&#xff0c;TypeScript零基础入门到实战全套教程】 https://www.bilibili.com/video/BV14Z4y1u7pi/?share_sourcecopy_web&vd_sourceb1cb921b73fe3808550eaf2224d1c155 目录 2、TypeScript初体验 2.1 安装编译TS的工…