C++复习

C++ 复习

1.C++和C语言

C++是在C语言基础上改进发展而来的,是C语言的一个超集

C++和C语言在除了面向对象的设计思想以外有什么不一样?

1. 头文件

  • C语言用例如 <stdio.h>的头文件
  • C++ 用的 <iostream> 意为输入输出流,用于实现更高层次的输入输出操作

2. 输入输出

  • 标准输入输出是利用<iostream>库中的 cin(input) 和 cout(output) 这两个流对象
    输入输出流可理解为河流,<<放入一艘船,>>捞出一艘船

  • C语言使用 printfscanf 进行输入输出操作

    而 C++ 则引入了流操作符 cincout,更加直观:

    1
    2
    cin >> x;
    cout << x << endl;//endline,结束行之意

3. 变量类型

C++ 增加了 bool 类型和 string 类型,扩展了变量的表示形式:

  • bool 类型:

    • 变量的值为布尔判断结果,即 true(真)或 false(假)
    • 其底层实质为整数:true 表示为 1false 表示为 0
  • string 类型:

    • string 是 C++ 提供的字符串类,包含许多便捷的工具成员(例如字符串拼接、查找等)

    • 在 C 语言中,字符串只能存储在 char 类型的变量或 char 类型数组中

      而 C++ 提供了更高层次的封装,可以直接使用 string 来处理字符串

C++ 在 C 语言的基础上还新增了 引用类型类类型

引用是已存在变量的别名,定义格式为:类型标识符 &引用名 = 目标变量名;
例如:int x = 200; int &y = x; // y 是 x 的引用
注意:

  1. 引用必须初始化
    声明引用时必须同时初始化,未初始化是错误的:int &y; // 错误,未初始化

  2. 引用类型必须一致
    引用的类型需与目标变量一致:float &y = x; // 错误,类型不一致

  3. 引用常量或表达式需加 const
    常量或表达式作为引用必须需加 const,且引用值不可更改:

    1
    2
    const int &y = 200;  // 合法
    int &y = 200; // 错误,缺少 const

4. 标准库文件和命名空间

C++ 引入命名空间的概念,命名空间用于解决全局变量或函数命名冲突的问题,提供更好的代码组织能力

  • 为什么程序开头需要写 using namespace std;

​ 在 C++ 中,命名空间用于避免不同库中函数或对象命名冲突的问题,就像使用工具箱一样

​ 假设我们有两套工具箱:一个是水工工具箱,另一个是电工工具箱

​ 它们可能都有一些同名的工具(例如“手套”),但用途和构造完全不同

​ 为了避免混淆,我们需要明确指定使用哪个工具箱中的工具

​ 在 C++ 中,std (standard) 是标准工具箱的名称,包含了标准库中的函数和对象(如 cincout

​ 使用命名空间有两种方式:

  • 在程序开头定义命名空间

    1
    using namespace std;

    这就相当于声明整个程序默认使用标准工具箱中的工具

  • 每次使用时加上命名空间前缀

    1
    2
    std::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. 非(!:最高优先级,用于取反操作

  2. 与(&&:中间优先级,两个条件都为真时结果为真

  3. 或(||:最低优先级,至少一个条件为真时结果为真

    1
    2
    bool a = true, b = false, c = !a || b && a; 
    // c = false (先算 !a, 再算 b && a, 最后 ||)

常变量

常变量const)是值不可更改的变量,在程序中提供只读特性,常用于保护数据不被意外修改。

  • 定义时初始化:常变量必须在声明时赋值。

  • 作用:提高代码的安全性和可读性。

    1
    2
    const int maxValue = 100;  // 定义常变量
    // maxValue = 200; // 错误:常变量的值不能修改

引用类型!!!

在C++中,引用类型是一个变量的别名,通过它可以直接访问另一个变量。引用提供了对变量的一种间接访问方式,同时也保证了引用始终指向同一个变量(不可更改绑定对象)。以下是对C++中引用类型的详细介绍:

1. 定义和语法

引用通过在变量名前加上 & 来声明:

1
type &reference_name = variable_name;
  • type:引用的目标变量的类型。
  • &:表示这是一个引用类型。
  • reference_name:引用的名字。
  • variable_name:被引用的目标变量。

2. 特性

  • 引用必须在定义时初始化

    1
    2
    3
    int a = 10;
    int &ref = a; // 正确
    int &ref2; // 错误,引用必须初始化
  • 引用不可重新绑定: 一旦引用初始化为某个变量,它就不能再引用其他变量。

    1
    2
    3
    4
    int a = 10, b = 20;
    int &ref = a;
    ref = b; // 赋值给引用,修改的是 a 的值,而不是重新绑定 ref
    cout << a; // 输出 20
  • 引用本身没有独立存储: 引用只是原变量的别名,不占用额外的内存空间。

3. 引用的用途

(1)用作函数参数

引用常用于函数参数传递,以避免拷贝大对象,并允许函数修改实参。

1
2
3
4
5
6
7
8
9
10
void increment(int &x) {
x++;
}

int main() {
int a = 5;
increment(a);
cout << a; // 输出 6
return 0;
}
  • 传递引用避免了值传递的开销。
  • 引用传递允许函数直接修改调用者的变量。

(2)用作函数返回值

引用可以作为函数返回值,允许函数返回调用者可以操作的变量。

1
2
3
4
5
6
7
8
9
10
int& findMax(int &a, int &b) {
return (a > b) ? a : b;
}

int main() {
int x = 10, y = 20;
findMax(x, y) = 100; // 修改返回的引用值
cout << x << " " << y; // 输出 10 100
return 0;
}
  • 返回引用时,确保被引用的变量在函数返回后仍然有效

(3)在范围 for 循环中

引用可以用于遍历容器时避免拷贝,提高效率。

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

int main() {
vector<int> vec = {1, 2, 3, 4};

for (int &x : vec) {
x *= 2; // 通过引用直接修改元素
}

for (const int &x : vec) {
cout << x << " "; // 输出 2 4 6 8
}

return 0;
}

(4)常量引用

常量引用(const 引用)用于防止修改被引用的变量,通常用于函数参数传递。

1
2
3
4
5
6
7
8
9
10
void print(const int &x) {
// x = 10; // 错误,const 引用不可修改
cout << x << endl;
}

int main() {
int a = 5;
print(a); // 输出 5
return 0;
}
  • 常量引用的特点:
    • 可绑定到临时对象(右值)。
    • 保护目标变量不被修改。

4. 引用与指针的对比

特性 引用 指针
初始化 必须在定义时初始化。 可以定义后初始化(赋值)。
绑定 一旦绑定,无法更改引用的目标。 可以指向不同的对象。
语法简洁性 使用简单,与普通变量操作类似。 需要显式使用 *& 进行解引用或取地址。
空值支持 引用必须绑定到合法的对象。 指针可以为空(指向 nullptr)。
存储 不占用额外内存,是目标变量的别名。 占用独立内存空间,用来存储目标变量的地址。
右值绑定 常量引用支持绑定到右值。 需要特殊指针类型(如 const int*)绑定右值。

5. 示例:引用的综合应用

以下示例展示了引用在参数传递、返回值、const 修饰等场景的使用:

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

void modify(int &x) {
x *= 2; // 修改引用的变量
}

const int& getValue() {
static int val = 42;
return val; // 返回 const 引用
}

int main() {
int a = 10;
modify(a); // 通过引用传递
cout << "After modify: " << a << endl; // 输出 20

const int &ref = getValue();
cout << "Value: " << ref << endl; // 输出 42

return 0;
}

输出

1
2
After modify: 20
Value: 42

总结

  1. 引用的本质是变量的别名,提供对变量的一种间接访问方式。
  2. 必须初始化,且绑定后不能更改引用的目标。
  3. 应用场景广泛,如函数参数、函数返回值、const 引用保护数据。
  4. 与指针相比,引用语法更简洁,但灵活性略低。

2.数组、指针、引用和函数的使用

1 回顾函数概念

函数是执行特定任务的一段代码,通过调用它可以实现代码复用

伪代码示例:

1
2
3
4
5
6
// 函数定义
int 加法(int a, int b){
return 参数1 + 参数2
}
// 函数调用
int a = 加法(3, 5) // 结果 = 8

2 默认参数

默认参数允许在函数定义时为某些参数提供默认值,调用时可选择性地忽略这些参数

1
2
3
int add(int a, int b = 5) { return a + b; }  // b 默认值为 5
int result = add(3); // result = 8
int result = add(3,4); // result = 7

注意:除函数形参外,其他引用定义时必须赋初始值

3 引用传参

引用传参&)让函数直接操作原变量,而不是其副本,提高效率并支持修改实参值:

1
2
3
void increment(int &x) { x++; }  
int a = 5;
increment(a); // a = 6

如果不用引用传参,可以通过返回修改后的值,并将结果重新赋值给变量来间接实现效果:

1
2
3
int increment(int x) { return x + 1; }  
int a = 5;
a = increment(a); // a = 6

虽然这种方法也能改变原变量的值,但每次调用函数都需要显式赋值,代码冗长且容易出错。

而引用传参不仅简洁,还能直接操作变量,避免不必要的复制和赋值操作

4 函数重载

函数重载和Java一样,允许在同一作用域内定义多个同名函数

参数列表(参数数量或类型)必须不同

1
2
3
4
5
6
7
int multiply(int a, int b) { return a * b; }  //原函数

double multiply(double a, double b) { return a * b; } //传入参数不同
double multiply(int a, int b) { return a * b; } //这是错的,要改的是参数列表

int result = multiply(2, 3); // result = 6
double result2 = multiply(2.5,2.0); // result2 = 5.0

5 内联函数

内联函数通过在调用处直接插入函数代码,减少函数调用开销,但只适用于小型函数

1
2
inline int square(int x) { return x * x; }  
int result = square(4); // result = 16

3.抽象与封装

1 面向对象思想

面向对象思想是一种以对象为中心的编程方式,强调抽象、封装、继承和多态四大特性

  • 抽象:从现实世界中提取关键属性和行为,忽略细节
  • 封装:隐藏对象内部实现细节,仅暴露必要接口
  • 继承:通过复用已有类的特性实现代码复用和扩展
  • 多态:允许对象表现出多种形态,提高程序灵活性

2 类和 UML 图

是对现实中对象的抽象,UML 图是用于表示类的结构和关系的工具
一个类的 UML 图通常包含以下部分:

  1. 类名
  2. 属性(成员变量)
  3. 方法(成员函数)

示例 UML 图:

1
2
3
4
5
6
7
8
9
+---------------+
| Car | // 类名
+---------------+
| - color | // 属性(私有)
| - speed |
+---------------+
| + start() | // 方法(公有)
| + stop() |
+---------------+

3 类和对象实现

1. 定义和作用

  • 公有(public):所有类和对象都能访问
  • 私有(private):只能被类的内部访问
  • 保护(protected):仅类内部及其子类可访问

2. 类定义和实例化

1
2
3
4
5
6
7
8
9
class Car {  
public:
string color;
void start() { cout << "启动车" << endl; }
};

Car myCar; // 实例化对象
myCar.color = "red"; // 访问成员变量
myCar.start(); // 调用成员函数

3. 构造/析构函数

都和类名字一样,一个是用于初始化,一个用于清内存资源.

  • 构造函数:在对象创建时自动调用,用于初始化对象
  • 析构函数:在对象销毁时自动调用,用于释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class Person {
public:
string *name; // 动态分配的内存
int age;

// 构造函数,函数名和类名一样
Person(string n, int a) {
name = new string(n); // 动态分配内存
age = a;
cout << "构造函数调用,分配资源" << endl;
}

// 析构函数,在类名基础上加了~标记
~Person() {
delete name; // 释放动态分配的内存
cout << "析构函数调用,释放资源" << endl;
}

void display() { cout << *name << ", " << age << endl; }
};

int main() {
Person p("Alice", 25); // 实例化对象,调用构造函数分内存
p.display(); // 输出:Alice, 25
return 0; // 在程序结束时调用析构函数
}

4 封装与读写接口

封装通过隐藏实现细节提高安全性,提供接口供外部访问或修改对象状态

  • 读接口:提供只读访问成员变量的方法
  • 写接口:提供修改成员变量的方法
1
2
3
4
5
6
7
8
9
10
11
12
class Car {  
private:
int speed;

public:
int getSpeed() { return speed; } // 读接口
void setSpeed(int s) { speed = s; } // 写接口
};

Car myCar;
myCar.setSpeed(80); // 写接口
cout << myCar.getSpeed(); // 读接口

5. 类模板

类模板是一种通用设计方式,用于根据不同的数据类型创建类,避免为每种类型重复编写代码。它通过模板参数实现灵活性,在实例化时根据传入的类型生成具体的类

  1. 模板声明:使用 template<class T> 定义模版
  2. 实例化:通过 <T> 指定具体类型,如 Compare<int>
  3. 优点:减少代码重复,适配多种数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

// 定义模板
template <typename T>//也可以写成<class T>,原因后面讲了
class Compare {
public:
Compare(T a, T b) : x(a), y(b) {} // T是个占位符,代替了原本的参数类型的表示
T max() { return (x > y) ? x : y; } // T是个占位符,代替了原本的函数返回的参数类型表示
private:
T x, y; // 模板类型的成员变量
};

int main() {
// 比较两个整数
Compare<int> cmp1(3, 7);//调用类的时候,在尖括号里传入参数
cout << cmp1.max() << " 是两个整数中较大的值" << endl;

// 比较两个字符
Compare<char> cmp2('a', 'g');
cout << cmp2.max() << " 是两个字符中较大的字符" << endl;

// 比较两个浮点数
Compare<float> cmp3(1.0f, 3.0f);
cout << cmp3.max() << " 是两个浮点数中较大的值" << endl;

return 0;
}

classtypename的区别

虽然在模板参数中 classtypename 是等价的,但在其他场景中,两者有细微的区别:

  1. typename 在嵌套依赖类型中的用途: 在使用模板时,可能会遇到嵌套依赖的情况,这时必须使用 typename 来指示一个依赖类型是一个“类型”,否则编译器可能会报错:

    1
    2
    3
    4
    template <typename T>
    class Example {
    typename T::NestedType value; // 必须使用 typename
    };

    如果改为 class,编译器会报错,因为 class 不能用于明确说明嵌套依赖类型。

  2. class 的语义歧义:

    • 在模板参数中,class 不一定表示它是一个“类”,它可以是任何类型(包括基本类型如 intdouble)。
    • typename 则更直观地表示“类型”。
  3. typename 更符合现代风格: 随着 C++ 标准的演进(尤其是 C++11 和之后的标准),typename 在语义上更准确,现代代码更倾向于使用 typename

为什么会有两种关键字?

  1. 历史原因: 在 C++ 最初的设计中,class 已经是一个关键词,并用于定义模板参数。后来为了更清晰地表示“类型”,在 C++98 标准中引入了 typename
  2. 向后兼容性: C++ 没有移除 class 用法,是为了兼容老代码。任何支持 C++98 或更高版本的编译器都会支持两者。

4. 数据共享和保护

4.1 静态成员变量

静态成员变量属于类本身,而非类的具体对象,所有对象共享该变量

  • 只初始化一次,在整个程序运行期间都存在
1
2
3
4
5
6
7
8
class Counter {
public:
static int count; // 静态成员变量声明
Counter() { count++; }
};

int Counter::count = 0; // 静态成员变量初始化
Counter c1, c2; // c1 和 c2 共享 count变量,最终 count = 2

4.2 静态成员函数

静态成员函数只能访问静态成员变量或其他静态成员函数

主要是为了引出本类中的静态成员变量

  • 不需要依赖具体对象,通过类直接调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter {
public:
static int count;
static void showCount() { cout << count << endl; }
};

// 在main函数中可以直接通过类名访问静态成员变量
Counter::count = 10;
Counter::showCount(); // 输出:10

// 也可以通过实例化对象访问静态成员变量(不推荐,但合法)
//这看起来像是访问对象的成员,但实际上还是修改类的静态成员,所有对象都会受到影响
Counter obj;
obj.count = 20;
obj.showCount(); // 输出:20

4.3 友元函数

可以理解为在类外面定义的属于类中的函数方法

友元函数通过关键字 friend 声明,可以访问类的私有和保护成员

  • 友元不是类成员,但拥有类的访问权限
1
2
3
4
5
6
7
8
9
10
11
class Box {
private:
int width;
public:
Box(int w) : width(w) {}
friend void showWidth(Box b); // 友元函数声明
};

void showWidth(Box b) { cout << b.width << endl; } // 定义
Box b(5);
showWidth(b); // 输出:5

4.4 友元类

友元类通过 friend class 声明,允许另一个类访问当前类的私有和保护成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Box {
private:
int width;
friend class Display; // 友元类声明
public:
Box(int w) : width(w) {}
};

class Display {
public:
void show(Box b) { cout << b.width << endl; }
};

Box b(7);
Display d;
d.show(b); // 输出:7

5. 继承与多态

1. 继承的概念

继承是面向对象编程的核心特性之一,允许子类从父类继承属性和行为,从而实现代码复用

  • 父类(基类):提供通用属性和行为
  • 子类(派生类):继承父类并扩展或重写其功能

2. 派生与继承

  • 派生类定义:通过 : 指定继承的父类

  • 访问控制:

    • 根据访问权限总结出不同的访问类型,如下所示:

      访问 public protected private
      同一个类 yes yes yes
      派生类 yes yes no
      外部的类 yes no no

      一个派生类继承了所有的基类方法,但下列情况除外:

      • 基类的构造函数、析构函数和拷贝构造函数。
      • 基类的重载运算符。
      • 基类的友元函数。
  • 继承类型

    当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。

    继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。

    几乎不使用 protectedprivate 继承,通常使用 public 继承

    当使用不同类型的继承时,遵循以下几个规则:

    • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
    • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
    • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
int x;
protected:
int y;
private:
int z;
};

class Derived : public Base { // 公有继承
public:
void display() { cout << x << endl; } // 只能访问 x 和 y
};

3. 多态

多态是面向对象编程的核心特性之一,它允许同一个接口在不同情况下表现出不同的行为,分为静态多态动态多态

1.静态多态

静态多态在编译时决定具体的函数调用,通常通过函数重载运算符重载实现

函数重载(在2.4部分有说):

在同一作用域内定义多个同名函数,但参数列表(参数数量或类型)必须不同

1
2
3
4
5
6
7
8
9
class Calculator {
public:
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
};

Calculator calc;
cout << calc.add(2, 3) << endl; // 输出:5
cout << calc.add(2.5, 3.5) << endl; // 输出:6.0
运算符重载

是静态多态的一种形式,允许重新定义运算符的行为,使其适用于自定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Complex {
public:
int real, imag;

Complex(int r, int i) : real(r), imag(i) {}

Complex operator+(const Complex &c) { //和函数重载方式一样,只是在运算符左边加上了operator标识
return Complex(real + c.real, imag + c.imag);
}
};

Complex c1(2, 3), c2(4, 5), c3 = c1 + c2;
cout << c3.real << " + " << c3.imag << "i" << endl; // 输出:6 + 8i

2.动态多态

动态多态在运行时决定具体的函数调用,通常通过虚函数实现。这种机制依赖于动态绑定(运行时绑定)

虚函数

比正常函数后面加了个virtual表示虚拟,是实现动态多态的关键

允许在基类中定义通用接口,并在派生类中重写具体实现

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void show() { cout << "基类" << endl; } //虚函数
};

class Derived : public Base {
public:
void show() override { cout << "衍生类" << endl; }
};

Base *obj = new Derived();
obj->show(); // 输出:衍生类
纯虚函数与抽象类
  1. 纯虚函数就是没有具体实现的虚函数,用于强制派生类提供自己的实现。

    在函数声明后加 = 0 即为纯虚函数

  2. 包含纯虚函数的类称为抽象类,无法直接实例化,它用于作为基类提供统一的接口

1
2
3
4
5
6
7
8
9
10
11
12
class Shape {  //抽象类,类似于java的接口类
public:
virtual void draw() = 0; // 纯虚函数,没有函数定义
};

class Circle : public Shape {
public:
void draw() override { cout << "画个圆" << endl; }
};

Shape *shape = new Rectangle();
shape->draw(); // 输出:画个圆

意义:抽象类定义了规范或协议,使派生类必须实现具体的行为,从而保证代码的一致性和灵活性