C++复习

C++复习
SkyforeverC++ 复习
1.C++和C语言
C++是在C语言基础上改进发展而来的,是C语言的一个超集
C++和C语言在除了面向对象的设计思想以外有什么不一样?
1. 头文件
- C语言用例如
<stdio.h>
的头文件 - C++ 用的
<iostream>
意为输入输出流,用于实现更高层次的输入输出操作
2. 输入输出流
-
标准输入输出是利用
<iostream>
库中的cin
(input) 和cout
(output) 这两个流对象
输入输出流可理解为河流,<<
放入一艘船,>>
捞出一艘船 -
C语言使用
printf
和scanf
进行输入输出操作而 C++ 则引入了流操作符
cin
和cout
,更加直观:1
2cin >> x;
cout << x << endl;//endline,结束行之意
3. 变量类型
C++ 增加了 bool
类型和 string
类型,扩展了变量的表示形式:
-
bool
类型:- 变量的值为布尔判断结果,即
true
(真)或false
(假) - 其底层实质为整数:
true
表示为1
,false
表示为0
- 变量的值为布尔判断结果,即
-
string
类型:-
string
是 C++ 提供的字符串类,包含许多便捷的工具成员(例如字符串拼接、查找等) -
在 C 语言中,字符串只能存储在
char
类型的变量或char
类型数组中而 C++ 提供了更高层次的封装,可以直接使用
string
来处理字符串
-
C++ 在 C 语言的基础上还新增了 引用类型 和 类类型。
引用是已存在变量的别名,定义格式为:类型标识符 &引用名 = 目标变量名;
例如:int x = 200; int &y = x; // y 是 x 的引用
注意:
-
引用必须初始化
声明引用时必须同时初始化,未初始化是错误的:int &y; // 错误,未初始化
-
引用类型必须一致
引用的类型需与目标变量一致:float &y = x; // 错误,类型不一致
-
引用常量或表达式需加
const
常量或表达式作为引用必须需加const
,且引用值不可更改:1
2const int &y = 200; // 合法
int &y = 200; // 错误,缺少 const
4. 标准库文件和命名空间
C++ 引入命名空间的概念,命名空间用于解决全局变量或函数命名冲突的问题,提供更好的代码组织能力
- 为什么程序开头需要写
using namespace std;
?
在 C++ 中,命名空间用于避免不同库中函数或对象命名冲突的问题,就像使用工具箱一样
假设我们有两套工具箱:一个是水工工具箱,另一个是电工工具箱
它们可能都有一些同名的工具(例如“手套”),但用途和构造完全不同
为了避免混淆,我们需要明确指定使用哪个工具箱中的工具
在 C++ 中,std
(standard) 是标准工具箱的名称,包含了标准库中的函数和对象(如 cin
和 cout
)
使用命名空间有两种方式:
-
在程序开头定义命名空间:
1
using namespace std;
这就相当于声明整个程序默认使用标准工具箱中的工具
-
每次使用时加上命名空间前缀:
1
2std::cin >> x;
std::cout << x << std::endl;这样显式地指定工具来自于标准工具箱
std
如果没有正确指定命名空间,程序可能会出错
例如电工手套需要绝缘透气,而水工手套需要完全防水,混用会导致问题
同样,程序无法识别函数或对象的来源时也会产生错误
自增和自减运算符
自增(++
)和自减(--
)运算符用于对变量进行快速的加 1 或减 1 操作
-
前置形式(
++x
或--x
):变量先增减,再参与表达式运算 -
后置形式(
x++
或x--
):变量先参与表达式运算,再增减1
int x = 5, y = ++x, z = x--; // y = 6, z = 6, x = 5
逻辑运算符
逻辑运算符用于对布尔值进行逻辑运算,按优先级从高到低排列:
-
非(
!
):最高优先级,用于取反操作 -
与(
&&
):中间优先级,两个条件都为真时结果为真 -
或(
||
):最低优先级,至少一个条件为真时结果为真1
2bool a = true, b = false, c = !a || b && a;
// c = false (先算 !a, 再算 b && a, 最后 ||)
常变量
常变量(const
)是值不可更改的变量,在程序中提供只读特性,常用于保护数据不被意外修改。
-
定义时初始化:常变量必须在声明时赋值。
-
作用:提高代码的安全性和可读性。
1
2const int maxValue = 100; // 定义常变量
// maxValue = 200; // 错误:常变量的值不能修改
引用类型!!!
在C++中,引用类型是一个变量的别名,通过它可以直接访问另一个变量。引用提供了对变量的一种间接访问方式,同时也保证了引用始终指向同一个变量(不可更改绑定对象)。以下是对C++中引用类型的详细介绍:
1. 定义和语法
引用通过在变量名前加上 &
来声明:
1 | type &reference_name = variable_name; |
type
:引用的目标变量的类型。&
:表示这是一个引用类型。reference_name
:引用的名字。variable_name
:被引用的目标变量。
2. 特性
-
引用必须在定义时初始化:
1
2
3int a = 10;
int &ref = a; // 正确
int &ref2; // 错误,引用必须初始化 -
引用不可重新绑定: 一旦引用初始化为某个变量,它就不能再引用其他变量。
1
2
3
4int a = 10, b = 20;
int &ref = a;
ref = b; // 赋值给引用,修改的是 a 的值,而不是重新绑定 ref
cout << a; // 输出 20 -
引用本身没有独立存储: 引用只是原变量的别名,不占用额外的内存空间。
3. 引用的用途
(1)用作函数参数
引用常用于函数参数传递,以避免拷贝大对象,并允许函数修改实参。
1 | void increment(int &x) { |
- 传递引用避免了值传递的开销。
- 引用传递允许函数直接修改调用者的变量。
(2)用作函数返回值
引用可以作为函数返回值,允许函数返回调用者可以操作的变量。
1 | int& findMax(int &a, int &b) { |
- 返回引用时,确保被引用的变量在函数返回后仍然有效。
(3)在范围 for
循环中
引用可以用于遍历容器时避免拷贝,提高效率。
1 |
|
(4)常量引用
常量引用(const
引用)用于防止修改被引用的变量,通常用于函数参数传递。
1 | void print(const int &x) { |
- 常量引用的特点:
- 可绑定到临时对象(右值)。
- 保护目标变量不被修改。
4. 引用与指针的对比
特性 | 引用 | 指针 |
---|---|---|
初始化 | 必须在定义时初始化。 | 可以定义后初始化(赋值)。 |
绑定 | 一旦绑定,无法更改引用的目标。 | 可以指向不同的对象。 |
语法简洁性 | 使用简单,与普通变量操作类似。 | 需要显式使用 * 和 & 进行解引用或取地址。 |
空值支持 | 引用必须绑定到合法的对象。 | 指针可以为空(指向 nullptr )。 |
存储 | 不占用额外内存,是目标变量的别名。 | 占用独立内存空间,用来存储目标变量的地址。 |
右值绑定 | 常量引用支持绑定到右值。 | 需要特殊指针类型(如 const int* )绑定右值。 |
5. 示例:引用的综合应用
以下示例展示了引用在参数传递、返回值、const
修饰等场景的使用:
1 |
|
输出:
1 | After modify: 20 |
总结
- 引用的本质是变量的别名,提供对变量的一种间接访问方式。
- 必须初始化,且绑定后不能更改引用的目标。
- 应用场景广泛,如函数参数、函数返回值、
const
引用保护数据。 - 与指针相比,引用语法更简洁,但灵活性略低。
2.数组、指针、引用和函数的使用
1 回顾函数概念
函数是执行特定任务的一段代码,通过调用它可以实现代码复用
伪代码示例:
1 | // 函数定义 |
2 默认参数
默认参数允许在函数定义时为某些参数提供默认值,调用时可选择性地忽略这些参数
1 | int add(int a, int b = 5) { return a + b; } // b 默认值为 5 |
注意:除函数形参外,其他引用定义时必须赋初始值
3 引用传参
引用传参(&
)让函数直接操作原变量,而不是其副本,提高效率并支持修改实参值:
1 | void increment(int &x) { x++; } |
如果不用引用传参,可以通过返回修改后的值,并将结果重新赋值给变量来间接实现效果:
1 | int increment(int x) { return x + 1; } |
虽然这种方法也能改变原变量的值,但每次调用函数都需要显式赋值,代码冗长且容易出错。
而引用传参不仅简洁,还能直接操作变量,避免不必要的复制和赋值操作
4 函数重载
函数重载和Java一样,允许在同一作用域内定义多个同名函数
但参数列表(参数数量或类型)必须不同
1 | int multiply(int a, int b) { return a * b; } //原函数 |
5 内联函数
内联函数通过在调用处直接插入函数代码,减少函数调用开销,但只适用于小型函数
1 | inline int square(int x) { return x * x; } |
3.抽象与封装
1 面向对象思想
面向对象思想是一种以对象为中心的编程方式,强调抽象、封装、继承和多态四大特性
- 抽象:从现实世界中提取关键属性和行为,忽略细节
- 封装:隐藏对象内部实现细节,仅暴露必要接口
- 继承:通过复用已有类的特性实现代码复用和扩展
- 多态:允许对象表现出多种形态,提高程序灵活性
2 类和 UML 图
类是对现实中对象的抽象,UML 图是用于表示类的结构和关系的工具
一个类的 UML 图通常包含以下部分:
类名属性(成员变量)方法(成员函数)
示例 UML 图:
1 | +---------------+ |
3 类和对象实现
1. 定义和作用
- 公有(public):所有类和对象都能访问
- 私有(private):只能被类的内部访问
- 保护(protected):仅类内部及其子类可访问
2. 类定义和实例化
1 | class Car { |
3. 构造/析构函数
都和类名字一样,一个是用于初始化,一个用于清内存资源.
- 构造函数:在对象创建时自动调用,用于初始化对象
- 析构函数:在对象销毁时自动调用,用于释放资源
1 |
|
4 封装与读写接口
封装通过隐藏实现细节提高安全性,提供接口供外部访问或修改对象状态
- 读接口:提供只读访问成员变量的方法
- 写接口:提供修改成员变量的方法
1 | class Car { |
5. 类模板
类模板是一种通用设计方式,用于根据不同的数据类型创建类,避免为每种类型重复编写代码。它通过模板参数实现灵活性,在实例化时根据传入的类型生成具体的类
- 模板声明:使用
template<class T>
定义模版 - 实例化:通过
<T>
指定具体类型,如Compare<int>
- 优点:减少代码重复,适配多种数据类型
1 |
|
class
和 typename
的区别
虽然在模板参数中 class
和 typename
是等价的,但在其他场景中,两者有细微的区别:
-
typename
在嵌套依赖类型中的用途: 在使用模板时,可能会遇到嵌套依赖的情况,这时必须使用typename
来指示一个依赖类型是一个“类型”,否则编译器可能会报错:1
2
3
4template <typename T>
class Example {
typename T::NestedType value; // 必须使用 typename
};如果改为
class
,编译器会报错,因为class
不能用于明确说明嵌套依赖类型。 -
class
的语义歧义:- 在模板参数中,
class
不一定表示它是一个“类”,它可以是任何类型(包括基本类型如int
和double
)。 typename
则更直观地表示“类型”。
- 在模板参数中,
-
typename
更符合现代风格: 随着 C++ 标准的演进(尤其是 C++11 和之后的标准),typename
在语义上更准确,现代代码更倾向于使用typename
。
为什么会有两种关键字?
- 历史原因: 在 C++ 最初的设计中,
class
已经是一个关键词,并用于定义模板参数。后来为了更清晰地表示“类型”,在 C++98 标准中引入了typename
。 - 向后兼容性: C++ 没有移除
class
用法,是为了兼容老代码。任何支持 C++98 或更高版本的编译器都会支持两者。
4. 数据共享和保护
4.1 静态成员变量
静态成员变量属于类本身,而非类的具体对象,所有对象共享该变量
- 只初始化一次,在整个程序运行期间都存在
1 | class Counter { |
4.2 静态成员函数
静态成员函数只能访问静态成员变量或其他静态成员函数
主要是为了引出本类中的静态成员变量
- 不需要依赖具体对象,通过类直接调用
1 | class Counter { |
4.3 友元函数
可以理解为在类外面定义的属于类中的函数方法
友元函数通过关键字 friend
声明,可以访问类的私有和保护成员
- 友元不是类成员,但拥有类的访问权限
1 | class Box { |
4.4 友元类
友元类通过 friend class
声明,允许另一个类访问当前类的私有和保护成员
1 | class Box { |
5. 继承与多态
1. 继承的概念
继承是面向对象编程的核心特性之一,允许子类从父类继承属性和行为,从而实现代码复用
- 父类(基类):提供通用属性和行为
- 子类(派生类):继承父类并扩展或重写其功能
2. 派生与继承
-
派生类定义:通过
:
指定继承的父类 -
访问控制:
-
根据访问权限总结出不同的访问类型,如下所示:
访问 public protected private 同一个类 yes yes yes 派生类 yes yes no 外部的类 yes no no 一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
-
-
继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。
继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
几乎不使用 protected 或 private 继承,通常使用 public 继承
当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
1 | class Base { |
3. 多态
多态是面向对象编程的核心特性之一,它允许同一个接口在不同情况下表现出不同的行为,分为静态多态和动态多态
1.静态多态
静态多态在编译时决定具体的函数调用,通常通过函数重载或运算符重载实现
函数重载(在2.4部分有说):
在同一作用域内定义多个同名函数,但参数列表(参数数量或类型)必须不同
1 | class Calculator { |
运算符重载
是静态多态的一种形式,允许重新定义运算符的行为,使其适用于自定义类
1 | class Complex { |
2.动态多态
动态多态在运行时决定具体的函数调用,通常通过虚函数实现。这种机制依赖于动态绑定(运行时绑定)
虚函数
比正常函数后面加了个virtual
表示虚拟,是实现动态多态的关键
允许在基类中定义通用接口,并在派生类中重写具体实现
1 | class Base { |
纯虚函数与抽象类
-
纯虚函数就是没有具体实现的虚函数,用于强制派生类提供自己的实现。
在函数声明后加
= 0
即为纯虚函数 -
包含纯虚函数的类称为抽象类,无法直接实例化,它用于作为基类提供统一的接口
1 | class Shape { //抽象类,类似于java的接口类 |
意义:抽象类定义了规范或协议,使派生类必须实现具体的行为,从而保证代码的一致性和灵活性