C++基础汇总

文章目录

一、数据类型与运算符

(一)基本数据类型

在 C++ 中,基本数据类型是构建程序的基本单元。它们定义了数据在内存中的大小和数据可以执行的操作。以下是 C++ 中常用的基本数据类型:

1、整数类型:

short: 短整数,通常占用 16 位。
int: 整数,通常占用 32 位。
long: 长整数,通常占用 32 位或 64 位,取决于编译器和操作系统。
long long: 长长整数,通常占用 64 位。
还可以使用 signed 和 unsigned 关键字修饰整数类型,以表示它们是否包含负数。例如,unsigned int 类型表示一个无符号整数,它不能存储负数。

2、浮点类型:

float: 单精度浮点数,通常占用 32 位。
double: 双精度浮点数,通常占用 64 位。
long double: 扩展精度浮点数,通常占用 80 位或更多,取决于编译器和操作系统。

3、字符类型:

char: 字符,通常占用 8 位。char 可以表示一个 ASCII 字符。
wchar_t: 宽字符,通常占用 16 位或 32 位,取决于编译器和操作系统。wchar_t 可以表示一个 Unicode 字符。
char16_t: 适用于 UTF-16 编码的 16 位字符类型。
char32_t: 适用于 UTF-32 编码的 32 位字符类型。

4、布尔类型:

bool: 布尔类型,表示真或假。它的值可以是 true 或 false。

除了这些基本数据类型,C++ 还提供了一些复合数据类型,如数组、结构体、类、指针等。这些类型可以从基本数据类型和其他复合数据类型构建而来。

(二)变量与常量

变量和常量是在程序中用于存储和操作数据的实体。它们的主要区别在于是否允许修改它们的值。

1、变量

变量是一个具有特定数据类型的存储位置,其值可以在程序执行过程中更改。要声明一个变量,需要指定其数据类型,然后提供一个标识符(变量名)。例如:

int age; // 声明一个整型变量名为 age
float weight; // 声明一个浮点型变量名为 weight

可以在声明变量时为其赋予一个初始值:

int age = 25;
float weight = 68.5f;

在程序中,可以通过变量名来访问和修改变量的值:

age = 30; // 修改 age 的值为 30
weight = 70.0f; // 修改 weight 的值为 70.0

2、常量

常量是一个具有特定数据类型的存储位置,其值在程序执行过程中不允许更改。常量在声明时必须赋予一个初始值。在 C++ 中,可以使用 const 关键字声明常量。例如:

const int daysInYear = 365; // 声明一个整型常量名为 daysInYear
const float pi = 3.14159f; // 声明一个浮点型常量名为 pi

一旦为常量分配了值,就不能再修改它:

daysInYear = 366; // 这会导致编译错误,因为不能修改常量的值

总结:
变量和常量都是用于存储数据的实体,但变量允许在程序执行过程中更改其值,而常量则不允许。在编写程序时,应根据需求合理地选择使用变量还是常量。对于不需要修改的值,使用常量可以提高程序的可读性和可维护性。。

(三)运算符与表达式

在 C++ 程序中,运算符和表达式用于执行各种计算和操作。

1、运算符

运算符是一种用于操作一个或多个操作数(变量、常量、表达式等)的符号。C++ 提供了多种类型的运算符,包括:

  • 算术运算符:用于执行基本的数学运算,如加法、减法、乘法、除法和取模。例如:+, -, *, /, %。

  • 关系运算符:用于比较两个值,如等于、不等于、大于、小于、大于等于、小于等于。例如:==, !=, >, <, >=, <=。

  • 逻辑运算符:用于执行逻辑操作,如与、或、非。例如:&&, ||, !。

  • 位运算符:用于执行位操作,如按位与、按位或、按位异或、按位取反、左移、右移。例如:&, |, ^, ~, <<, >>。

  • 赋值运算符:用于将一个值赋给变量,如简单赋值、加法赋值、减法赋值等。例如:=, +=, -=, *=, /=,%=。

  • 条件运算符:用于根据条件表达式选择两个值之一。例如:? :。

  • 逗号运算符:用于链接多个表达式,按顺序计算,并返回最后一个表达式的值。例如:,。

  • 类型转换运算符:用于将一种数据类型转换为另一种数据类型。例如:static_cast, dynamic_cast, const_cast,
    reinterpret_cast。

  • 其他运算符:例如:sizeof(返回数据类型或对象的大小)、new(分配内存)、delete(释放内存)等。

2、表达式

表达式是由一个或多个操作数和运算符组成的代码片段,用于计算一个值。表达式可以包含变量、常量、函数调用、对象成员访问等。例如:

int x = 5;
int y = 10;
int z = x + y; // 这里的 "x + y" 是一个表达式,包含两个操作数(x 和 y)和一个运算符(+)

bool isGreater = x > y; // 这里的 "x > y" 是一个表达式,包含两个操作数(x 和 y)和一个运算符(>)

int result = x * y + z; // 这里的 "x * y + z" 是一个表达式,包含三个操作数(x、y 和 z)和两个运算符(* 和 +)

表达式可以嵌套在其他表达式中,形成更复杂的计算:

int a = 1;
int b
int b = 2;
int c = 3;

int result = (a + b) * c; // 这里的 "(a + b) * c" 是一个表达式,包含一个嵌套的表达式 "a + b"

bool isTrue = (a < b) && (b < c); // 这里的 "(a < b) && (b < c)" 是一个表达式,包含两个嵌套的表达式 "a < b" 和 "b < c"

(四)类型转换

在 C++ 中,类型转换是将一个数据类型的值转换为另一个数据类型的值。类型转换可以分为隐式类型转换和显式类型转换。

1、隐式类型转换

隐式类型转换是编译器自动执行的类型转换。在某些情况下,编译器会自动将一种类型的值转换为另一种类型。例如,将一个整数转换为浮点数,或者将一个派生类对象的指针或引用转换为基类指针或引用。隐式类型转换的一个典型例子是:

int i = 42;
double d = i; // 隐式类型转换,将 int 类型的值转换为 double 类型

2、显式类型转换

显式类型转换是程序员明确请求的类型转换。C++ 提供了几种类型转换运算符,用于执行显式类型转换:
1、C 风格类型转换:使用圆括号和目标类型名称。例如:

int i = 42;
double d = (double)i; // 使用 C 风格类型转换将 int 类型的值转换为 double 类型 

2、static_cast:用于非多态类型之间的转换,例如基本类型、指针和引用。它执行编译时类型检查,确保转换是安全的。例如:

int i = 42;
double d = static_cast<double>(i); // 使用 static_cast 将 int 类型的值转换为 double 类型 

3、dynamic_cast:用于多态类型之间的转换,主要用于将基类指针或引用转换为派生类指针或引用。它执行运行时类型检查,确保转换是安全的。例如:

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 使用 dynamic_cast 将基类指针转换为派生类指针 

4、const_cast:用于移除或添加 const 限定符。例如:

const int ci = 42; 
int* pi = const_cast<int*>(&ci); // 使用 const_cast 移除 const 限定符,从 const int* 转换为 int* 

5、reinterpret_cast:用于低级别的类型转换,例如将一个指针转换为整数,或者将一个整数转换为指针。这种转换可能导致未定义的行为,因此应谨慎使用。例如:

int i = 42;
int* pi = &i; 
uintptr_t intptr = reinterpret_cast<uintptr_t>(pi); // 使用 reinterpret_cast 将 int* 类型的值转换为 uintptr_t 类型 

在使用显式类型转换时,应该尽量避免 C 风格类型转换,因为它可能导致未预期的行为。相反,应该使用 C++ 提供的类型转换运算符(如 static_cast、dynamic_cast、const_cast 和 reinterpret_cast),因为它们更加明确和安全。

在进行类型转换时,需要注意以下几点:

尽量避免不必要的类型转换,因为它们可能导致性能损失或降低代码的可读性。
当需要类型转换时,优先使用 C++ 风格的类型转换运算符,因为它们比 C 风格类型转换更明确和安全。
在使用多态类型时,尽量使用 dynamic_cast,因为它提供了运行时类型检查,确保类型转换的安全性。
不要滥用 reinterpret_cast,因为它可能导致未定义的行为。只有在确实需要进行低级别类型转换时,才应该使用它。
总之,类型转换是 C++ 编程中常见的操作,需要了解何时使用哪种类型转换以及如何避免潜在的问题。理解隐式和显式类型转换的区别,以及如何使用 C++ 提供的类型转换运算符,有助于编写高效、安全的代码。

二、流程控制

(一)条件语句

条件语句是根据某个条件的真或假来执行不同的代码块。在 C++ 中,有两种主要的条件语句:if 语句和 switch 语句。

1. if 语句

if 语句是最基本的条件语句。它的语法如下:

if (condition) {
    // 当条件为真(非零)时执行的代码
}

condition 是一个布尔表达式,当它的值为真(非零)时,执行大括号内的代码。例如:

int age = 18;
if (age >= 18) {
    std::cout << "You are an adult." << std::endl;
}

if 语句可以与 else 配合使用,当条件为假(零)时,执行 else 代码块:

if (condition) {
    // 当条件为真时执行的代码
} else {
    // 当条件为假时执行的代码
}

还可以使用 else if 来测试多个条件:

if (condition1) {
    // 当 condition1 为真时执行的代码
} else if (condition2) {
    // 当 condition1 为假,且 condition2 为真时执行的代码
} else {
    // 当 condition1 和 condition2 都为假时执行的代码
}

2. switch 语句

switch 语句用于基于一个整数或枚举类型的值执行不同的代码块。它的语法如下:

switch (expression) {
    case constant1:
        // 当 expression 等于 constant1 时执行的代码
        break;
    case constant2:
        // 当 expression 等于 constant2 时执行的代码
        break;
    // ...
    default:
        // 当 expression 与任何常量都不匹配时执行的代码
}

expression 是一个整数或枚举类型的表达式,case 后面的常量必须是该类型的常量表达式。break 语句用于跳出 switch 语句。如果不使用 break,执行会继续到下一个 case,这称为“贯穿”(fallthrough)。default 是可选的,用于处理没有匹配的情况。

例如:

int dayOfWeek = 1;
switch (dayOfWeek) {
    case 1:
        std::cout << "Monday" << std::endl;
        break;
    case 2:
        std::cout << "Tuesday" << std::endl;
        break;
    case 3:
        std::cout << "Wednesday" << std::endl;
        break;
    case 4:
        std::cout << "Thursday" << std::endl;
        break;
    case 5:
        std::cout << "Friday" << std::endl;
        break;
    case 6:
        std::cout << "Saturday" << std::endl;
        break;
    case 7:
        std::cout << "Sunday" << std::endl;
        break;
    default

(二)循环语句

循环语句允许您多次执行一段代码,直到满足特定条件。在 C++ 中,有三种主要的循环语句:for 循环、while 循环和 do-while 循环。

1. for 循环

for 循环用于执行特定次数的重复操作。它的语法如下:

for (initialization; condition; increment) {
    // 循环体(执行代码)
}

initialization 是在循环开始前执行的语句;condition 是在每次循环迭代之前测试的条件;increment 是在每次循环迭代之后执行的语句。例如:

for (int i = 0; i < 10; ++i) {
    std::cout << "Iteration: " << i << std::endl;
}

2. while 循环

while 循环用于在满足条件时重复执行一段代码。它的语法如下:

while (condition) {
    // 循环体(执行代码)
}

当 condition 为真(非零)时,执行循环体。例如:

int count = 0;
while (count < 10) {
    std::cout << "Count: " << count << std::endl;
    ++count;
}

3. do-while 循环

do-while 循环与 while 循环类似,但 do-while 循环至少执行一次循环体,因为条件在循环体之后测试。它的语法如下:

do {
    // 循环体(执行代码)
} while (condition);

例如:

int value;
do {
    std::cout << "Enter a value between 1 and 10: ";
    std::cin >> value;
} while (value < 1 || value > 10);

在使用循环时,注意避免无限循环,确保循环条件在某个时刻变为假。此外,在编写循环时,选择适当的循环类型以提高代码的可读性和效率。

三、函数

(一)函数的定义与声明

在 C++ 中,函数是一段可重用的代码,它可以接受输入参数,执行一系列操作,并返回一个结果。函数有助于将代码分解成更小的、可管理的部分,从而提高代码的可读性、可维护性和重用性。

函数声明(也称为函数原型)向编译器提供有关函数的信息,包括函数名、返回类型和参数列表。函数声明的语法如下:

return_type function_name(parameter_list);

例如,一个接受两个整数参数并返回它们和的函数声明如下:

int add(int a, int b);

通常,函数声明放在程序的开头或头文件中,以便其他函数在调用它们之前知道它们的存在。

函数定义包括函数的实际实现,即执行的代码。函数定义的语法如下:

return_type function_name(parameter_list) {
    // 函数体(执行代码)
}

例如,上面声明的 add 函数的定义如下:

int add(int a, int b) {
    return a + b;
}

函数定义通常放在源文件中。在大型项目中,可以将函数声明放在头文件中(以 .h 或 .hpp 结尾),将函数定义放在源文件中(以 .cpp 结尾)。

注意: 函数声明和定义中的参数列表应匹配。参数列表包括参数的类型和名称,但函数声明中的参数名称可以省略,只保留类型。例如,以下函数声明也是有效的:

int add(int, int);

然而,建议在声明中包含参数名称,以提高代码的可读性。

(二)函数的参数与返回值

在 C++ 中,函数参数是传递给函数的输入值。返回值是函数执行后产生的结果。参数和返回值可以是基本数据类型、对象或者指针。

函数参数

函数参数定义在函数声明和定义的括号中。参数列表包括参数的类型和名称。例如,以下函数接受两个整数参数:

int add(int a, int b);

在调用函数时,实参(调用时提供的值)按照参数列表的顺序传递给形参(函数定义中的参数变量)。默认情况下,参数是通过值传递的,这意味着函数使用参数值的副本,而不是原始变量。因此,在函数内部对参数进行的任何更改都不会影响原始变量。

函数返回值

函数返回值是函数执行后产生的结果。函数通过 return 语句返回结果。函数的返回类型在函数声明和定义的开始处指定。如果函数没有返回值,可以使用 void 关键字指定返回类型。

以下函数返回两个整数参数的和:

int add(int a, int b) {
    return a + b;
}

在这个例子中,返回类型是 int,表示函数将返回一个整数值。return 语句后面的表达式应与返回类型兼容。

以下是一个没有返回值的函数示例:

void print_hello() {
    std::cout << "Hello, world!" << std::endl;
}

在这个例子中,返回类型是 void,表示函数没有返回值。因此,不需要在函数体中包含 return 语句。

需要注意的是,使用引用或指针作为参数时,函数可以修改调用者的原始数据。这种情况下,虽然函数可能没有显式的返回值(即 void 类型的返回值),但它仍然可以对输入参数产生影响。

(三)函数重载

函数重载是 C++ 中的一个特性,允许在同一作用域中定义多个具有相同名称但参数列表不同的函数。编译器根据调用时提供的参数类型和数量选择正确的函数。这使得程序员能够以一种自然和易读的方式实现同一功能的不同变体,而无需为每个变体使用不同的函数名称。

以下是函数重载的一个示例:

#include <iostream>
#include <string>

// 函数重载示例:add() 函数

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

// 接受两个浮点数参数并返回它们的和
double add(double a, double b) {
    return a + b;
}

// 接受两个字符串参数并返回它们的连接
std::string add(const std::string& a, const std::string& b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2) << std::endl;          // 调用 int add(int, int) 函数
    std::cout << add(1.0, 2.0) << std::endl;      // 调用 double add(double, double) 函数
    std::cout << add("Hello, ", "world!") << std::endl; // 调用 std::string add(const std::string&, const std::string&) 函数

    return 0;
}

这个例子中定义了三个重载的 add 函数,分别处理整数、浮点数和字符串参数。在 main 函数中,根据提供的参数类型,编译器选择正确的 add 函数。

需要注意的是,函数重载仅根据参数列表的不同进行选择。返回类型不能作为重载函数的区分依据。换句话说,具有相同参数列表但不同返回类型的函数不能被视为重载函数。

函数重载的几个注意事项:

参数列表必须不同。这可能是参数数量的不同,或者相同数量参数的类型不同。
返回类型不能用于区分重载函数。
在调用过程中,如果编译器无法根据提供的参数类型和数量明确选择一个重载函数,将导致编译错误。这称为重载决议的歧义。

(四)内联函数

内联函数(Inline functions)是 C++ 中一种优化技术,旨在减少函数调用的开销。当一个函数被声明为内联时,编译器会尝试将函数的定义直接插入到调用它的地方,而不是通过常规的函数调用机制。这样可以减少函数调用时的开销,比如参数传递、返回地址的跳转等。

内联函数使用 inline 关键字声明,通常在函数声明或定义之前。内联函数通常很小,因为较大的函数可能导致代码膨胀,从而降低性能。需要注意的是,将函数声明为内联仅仅是向编译器发出请求,编译器可能会忽略这个请求。

以下是内联函数的一个示例:

#include <iostream>

// 内联函数声明及定义
inline int square(int x) {
    return x * x;
}

int main() {
    int num = 5;
    std::cout << "Square of " << num << " is " << square(num) << std::endl;

    return 0;
}

在这个例子中,square 函数被声明为内联。当编译器处理到 square 函数的调用时,它会尝试将函数体直接插入到调用位置。这样可以避免函数调用时的额外开销。然而,需要注意的是内联函数不能解决所有性能问题,并且过多地使用内联可能导致代码膨胀。因此,在决定将一个函数声明为内联时,应权衡函数调用的开销与代码膨胀之间的平衡。

(五)默认参数和占位参数

默认参数和占位参数是 C++ 函数定义中的两个特性,它们让函数调用更加灵活。

默认参数:
默认参数是函数定义中为某些参数提供的默认值。当调用函数时,如果没有为具有默认值的参数提供实际值,那么将使用这些默认值。默认参数可以简化函数调用,同时提高代码可读性。

默认参数应该在函数声明或定义时提供,且从右到左提供。换句话说,一旦一个参数被赋予默认值,那么其右边的所有参数也必须有默认值。

以下是一个使用默认参数的示例:

#include <iostream>

// 函数声明,提供默认参数
int add(int a, int b = 0, int c = 0);

int main() {
    std::cout << add(1) << std::endl;        // 输出 1,使用默认值 b = 0, c = 0
    std::cout << add(1, 2) << std::endl;     // 输出 3,使用默认值 c = 0
    std::cout << add(1, 2, 3) << std::endl;  // 输出 6,未使用默认值

    return 0;
}

// 函数定义,可以省略默认参数,因为已在声明时提供
int add(int a, int b, int c) {
    return a + b + c;
}

占位参数:
占位参数是指在函数参数列表中,使用了关键字 void 作为参数类型的参数。占位参数本身没有实际意义,不能在函数内部使用。占位参数主要用于强调特定的函数签名,或者作为将来可能添加参数的占位符。

以下是一个使用占位参数的示例:

#include <iostream>

// 函数声明,包含占位参数
void foo(int a, void*);

int main() {
    int a = 10;
    foo(a, nullptr);  // 调用函数,传递 nullptr 作为占位参数

    return 0;
}

// 函数定义,包含占位参数
void foo(int a, void* ptr) {
    std::cout << "Value of a: " << a << std::endl;
}

在这个例子中,foo 函数的第二个参数是一个占位参数。当调用 foo 函数时,我们传递了一个 nullptr 作为占位参数。在 foo 函数内部,占位参数没有被使用。请注意,在 C++ 中,占位参数的使用相对较少。

四、数组和字符串

(一)一维数组与多维数组

在 C++ 中,数组是一种数据结构,用于存储相同类型的连续元素。数组的大小在声明时是固定的,不能在运行时改变。以下是一维数组和多维数组的简要介绍:

一维数组:
一维数组是最简单的数组类型,可以将其视为一行元素。一维数组在声明时需要指定元素类型和数组大小。数组的大小必须是一个常量表达式。

以下是一个一维数组的示例:

#include <iostream>

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};  // 声明并初始化一个整数一维数组

    // 遍历数组并输出元素
    for (int i = 0; i < 5; ++i) {
        std::cout << numbers[i] << std::endl;
    }

    return 0;
}

多维数组:
多维数组可以看作是由一维数组组成的数组。最常见的多维数组是二维数组,可以将其视为一个表格,包含行和列。在声明多维数组时,需要为每个维度指定大小。

以下是一个二维数组的示例:

#include <iostream>

int main() {
    // 声明并初始化一个 3x4 的整数二维数组
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 遍历数组并输出元素
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << matrix[i][j] << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

在这个例子中,我们声明并初始化了一个 3x4 的整数二维数组。遍历二维数组时,需要使用两层循环,外层循环用于遍历行,内层循环用于遍历列。同样地,可以声明更高维度的数组,例如三维数组,四维数组等,但它们在实际编程中使用较少。

(二)字符串与字符数组

在 C++ 中,字符串和字符数组是处理文本数据的常用方式。这里我们将介绍字符数组和 C++ 标准库中的字符串类(std::string)。

字符数组:
字符数组是一个包含字符的一维数组。在 C++ 中,字符串字面量(例如 “Hello”)实际上是一个以空字符(‘\0’)结尾的字符数组。空字符用于表示字符串的结束,因此字符数组的实际大小比字符串中的字符数多 1。以下是一个字符数组的示例:

#include <iostream>

int main() {
    char message[] = "Hello, World!";  // 声明并初始化一个字符数组

    // 输出字符数组
    std::cout << message << std::endl;

    return 0;
}

注意,这里我们使用了字符数组的名称作为 std::cout 的参数,C++ 将自动输出字符数组中的所有字符,直到遇到空字符。

std::string:
C++ 标准库提供了一个名为 std::string 的字符串类,用于简化字符串操作。std::string 类提供了许多有用的字符串操作方法,如长度计算、子字符串查找、字符串拼接等。以下是一个 std::string 的示例:

#include <iostream>
#include <string>

int main() {
    std::string greeting = "Hello, World!";  // 声明并初始化一个 std::string 对象

    // 输出字符串
    std::cout << greeting << std::endl;

    // 计算字符串长度
    std::cout << "Length: " << greeting.length() << std::endl;

    return 0;
}

在这个例子中,我们声明并初始化了一个 std::string 对象,然后使用 length() 方法计算字符串的长度。与字符数组相比,std::string 更易于使用,提供了更多的功能,并支持动态大小。在现代 C++ 编程中,推荐使用 std::string 来处理字符串。

(三)字符串处理函数

C++ 中有许多字符串处理函数,大部分属于 C++ 标准库的一部分。这里我们将介绍一些常用的字符串处理函数。在 C++ 中,字符串可以用字符数组表示,也可以用 std::string 类表示。这里的示例将展示字符数组和 std::string 对象的处理方法。

1、字符串长度:

对于字符数组,可以使用 strlen 函数计算字符串长度。strlen 函数位于 (C++11 之前为 <string.h>)头文件中。

#include <iostream>
#include <cstring>

int main() {
    char str[] = "Hello, World!";
    std::cout << "Length: " << strlen(str) << std::endl;
    return 0;
}

对于 std::string 对象,可以使用 length() 或 size() 成员函数计算字符串长度。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    std::cout << "Length: " << str.length() << std::endl;
    return 0;
}

2、字符串拼接:

对于字符数组,可以使用 strcat 函数拼接字符串。注意,目标字符数组必须具有足够的空间容纳拼接后的字符串。strcat 函数位于 (C++11 之前为 <string.h>)头文件中。

#include <iostream>
#include <cstring>

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "World!";
    strcat(str1, str2);
    std::cout << "Concatenated: " << str1 << std::endl;
    return 0;
}

对于 std::string 对象,可以使用 + 运算符或 append() 成员函数拼接字符串。

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello, ";
    std::string str2 = "World!";
    str1 += str2;  // 或者使用 str1.append(str2);
    std::cout << "Concatenated: " << str1 << std::endl;
    return 0;
}

3、字符串比较:

对于字符数组,可以使用 strcmp 函数比较字符串。strcmp 函数返回 0 表示字符串相等,小于 0 表示第一个字符串小于第二个字符串,大于 0 表示第一个字符串大于第二个字符串。strcmp 函数位于 (C++11 之前为 <string.h>)头文件中。

#include <iostream>
#include <cstring>

int main() {
    char str1[] = "Hello";
    char str2[] = "World";

    if (strcmp(str1, str2) == 0) {
        std::cout << "Strings are equal." << std::endl;
    } else {
        std::cout << "Strings are not equal." << std::endl;
    }

    return 0;
}

对于 std::string 对象,可以使用 ==、<、> 等运算符或 compare() 成员函数比较字符串。

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = "World";

    if (str1 == str2) {
        std::cout << "Strings are equal." << std::endl;
    } else {
        std::cout << "Strings are not equal." << std::endl;
    }

    return 0;
}

或使用 compare() 成员函数:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = "World";

    if (str1.compare(str2) == 0) {
        std::cout << "Strings are equal." << std::endl;
    } else {
        std::cout << "Strings are not equal." << std::endl;
    }

    return 0;
}

4、字符串查找:

对于字符数组,可以使用 strstr 函数查找子字符串。strstr 函数返回一个指针,指向第一个匹配的子字符串的起始位置,如果没有找到匹配,则返回空指针。strstr 函数位于 (C++11 之前为 <string.h>)头文件中。

#include <iostream>
#include <cstring>

int main() {
    char str[] = "Hello, World!";
    char substr[] = "World";

    char *ptr = strstr(str, substr);
    if (ptr != nullptr) {
        std::cout << "Found substring at position: " << (ptr - str) << std::endl;
    } else {
        std::cout << "Substring not found." << std::endl;
    }

    return 0;
}

对于 std::string 对象,可以使用 find() 成员函数查找子字符串。find() 函数返回一个 std::string::size_type 类型的值,表示子字符串在原字符串中的位置。如果没有找到匹配,则返回 std::string::npos。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    std::string substr = "World";

    std::string::size_type pos = str.find(substr);
    if (pos != std::string::npos) {
        std::cout << "Found substring at position: " << pos << std::endl;
    } else {
        std::cout << "Substring not found." << std::endl;
    }

    return 0;
}

以上是一些常用的字符串处理函数和方法。实际上,C++ 标准库中还有很多其他的字符串处理功能,如 substr、insert、erase、replace 等。在实际编程中,可以根据需求选择适当的函数和方法。

五、指针与引用

(一)指针的定义与使用

指针是 C++ 中一种非常重要的数据类型,它存储了另一个变量的内存地址。指针允许我们通过引用内存地址来间接访问和操作变量。使用指针的原因有很多,如动态内存分配、函数参数传递、数据结构(如链表、树等)的实现等。

定义指针:
要定义一个指针,首先需要指定它所指向的数据类型,然后在变量名前加上一个星号(*)。例如:

int *intPtr;      // 定义一个指向整数的指针
double *dblPtr;   // 定义一个指向双精度浮点数的指针
char *charPtr;    // 定义一个指向字符的指针

使用指针:
为了使用指针,我们需要执行以下操作:

初始化指针:将某个变量的内存地址赋给指针。
解引用指针:通过指针访问所指向变量的值。
修改指针:改变指针所指向的内存地址。
下面的例子展示了指针的基本用法:

#include <iostream>

int main() {
    int x = 42;
    int *intPtr;      // 定义一个指向整数的指针

    intPtr = &x;       // 将 x 的内存地址赋给 intPtr

    std::cout << "Value of x: " << x << std::endl;
    std::cout << "Address of x: " << &x << std::endl;
    std::cout << "Value of intPtr: " << intPtr << std::endl;
    std::cout << "Value pointed by intPtr: " << *intPtr << std::endl;

    *intPtr = 100;     // 通过 intPtr 修改 x 的值

    std::cout << "New value of x: " << x << std::endl;

    return 0;
}

注意事项:

指针在使用前必须被初始化,否则它将包含一个未定义的内存地址,访问这个地址可能导致程序崩溃或其他未定义的行为。
当不再需要使用指针时,应当将其设置为 nullptr。这样可以避免悬空指针(指向已释放内存的指针)带来的问题。
访问已释放的内存或未分配的内存地址是危险的,可能导致程序崩溃或其他未定义的行为。
指针是 C++ 编程中一个非常强大的工具,但使用不当时也可能导致程序出现问题。因此,使用指针时一定要谨慎。

(二)指针运算与数组

指针和数组在 C++ 中有着密切的关系。实际上,数组名本身就是一个指针,它指向数组中的第一个元素。因此,指针和数组可以互相使用。让我们详细了解指针运算与数组的关系。

1、指针与数组名:

当你声明一个数组时,例如 int arr[5];,数组名 arr 是一个指向数组第一个元素的指针。所以,arr 和 &arr[0] 是等价的。

2、指针运算:

指针运算主要包括加法和减法。给定一个指针,你可以对其进行加法或减法操作,使其指向其他位置。例如:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr; // ptr 现在指向数组的第一个元素,即 arr[0]
ptr++; // ptr 现在指向数组的第二个元素,即 arr[1]
ptr += 2; // ptr 现在指向数组的第四个元素,即 arr[3]

需要注意的是,这种运算会考虑指针所指向类型的大小。例如,int 类型通常占用 4 个字节,所以 ptr++ 实际上会将指针向后移动 4 个字节。

3、使用指针访问数组元素:

你可以使用指针访问数组的元素。只需在指针前加一个星号(*),即可获取指针指向的值。例如:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr; // ptr 指向数组的第一个元素,即 arr[0]

int first_value = *ptr; // first_value 等于 arr[0],即 1

4、指针与数组下标:

你可以使用指针与下标一起访问数组元素。例如:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr; // ptr 指向数组的第一个元素,即 arr[0]

int second_value = ptr[1]; // second_value 等于 arr[1],即 2

总之,指针与数组在 C++ 中具有紧密的联系。你可以使用指针来访问和操作数组,同时利用指针运算在数组中移动。

(三)函数指针

函数指针是一种特殊类型的指针,它用于存储函数的地址,而不是变量或对象的地址。通过函数指针,你可以间接地调用函数,这在很多情况下非常有用,如回调函数、策略模式等。

让我们了解一下如何定义和使用函数指针。

1、定义函数指针:

要定义一个函数指针,你需要指定函数的返回类型、函数指针的名称以及函数参数列表。例如,假设你有一个如下的函数:

int add(int a, int b) {
    return a + b;
}

为了定义一个指向这个函数的指针,你可以这样做:

int (*add_ptr)(int, int);

2、初始化函数指针:

要初始化一个函数指针,你需要使用函数名,不带括号。例如:

add_ptr = add; // 将 add 函数的地址赋给 add_ptr

3、使用函数指针调用函数:

使用函数指针调用函数与使用普通函数名调用函数类似。只需将函数指针名称与参数列表放在一起即可。例如:

int result = add_ptr(3, 4); // 使用 add_ptr 调用 add 函数,result 的值为 7

这是一个完整的例子:

#include <iostream>

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*add_ptr)(int, int) = add; // 定义并初始化函数指针
    int result = add_ptr(3, 4); // 使用函数指针调用 add 函数
    std::cout << "Result: " << result << std::endl; // 输出:Result: 7

    return 0;
}

函数指针在某些情况下非常有用。例如,你可以将函数指针作为参数传递给其他函数,从而实现回调机制或策略模式等。

(四)引用的定义与使用

引用(Reference)是 C++ 中的一种类型别名,它为另一个已存在的变量或对象创建了一个别名。引用必须在声明时就初始化,并且在整个生命周期内不能重新指向另一个变量。引用的一个重要应用是在函数参数和返回值中,可以避免复制操作以提高效率,同时在函数内部修改实参的值。

1、定义引用:

定义引用时,需要使用 & 符号,接着是引用的类型和名称。引用必须在定义时初始化。例如:

int a = 10;
int &ref_a = a; // 定义一个 int 类型的引用 ref_a,并初始化为 a 的引用

2、使用引用:

引用的使用与普通变量相同。当你通过引用修改值时,实际上是在修改引用所指向的变量的值。例如:

#include <iostream>

int main() {
    int a = 10;
    int &ref_a = a; // 定义一个 int 类型的引用 ref_a,并初始化为 a 的引用

    std::cout << "a: " << a << ", ref_a: " << ref_a << std::endl; // 输出:a: 10, ref_a: 10

    ref_a = 20; // 通过引用修改 a 的值
    std::cout << "a: " << a << ", ref_a: " << ref_a << std::endl; // 输出:a: 20, ref_a: 20

    return 0;
}

3、引用作为函数参数:

引用可以作为函数参数,从而避免复制操作。当你将引用作为函数参数时,函数内部对引用的修改会影响到实参的值。

#include <iostream>

void increment(int &x) {
    x++;
}

int main() {
    int a = 10;
    increment(a); // 通过引用将 a 传递给函数 increment
    std::cout << "a: " << a << std::endl; // 输出:a: 11

    return 0;
}

4、引用作为函数返回值:

引用也可以作为函数的返回值,这样可以避免返回值的复制操作。不过要注意,返回局部变量的引用是危险的,因为局部变量在函数返回后会被销毁。所以,只有在确保引用指向的变量或对象在函数返回后仍然有效时,才可以返回引用。

#include <iostream>

int a = 10;

int &get_reference_to_a() {
    return a; // 返回 a 的引用
}

int main() {
    int &ref_a = get_reference_to_a(); // 获取 a 的引用
    std::cout << "a: " << a << ", ref_a: " << ref_a << std::endl; // 输出:a: 10, ref_a: 10

    ref_a = 20;
    std::cout << "a: " << a << ", ref_a: " << ref_a << std::endl; // 输出:a: 20, ref:20
    return 0;

5、引用与 const 关键字:

可以将 const 关键字与引用结合,从而创建一个常量引用。常量引用不能修改其引用的变量的值。常量引用在函数参数中尤为有用,因为它们可以保证在函数内部不会修改实参的值,同时避免了复制操作。

#include <iostream>

void print_value(const int &x) {
    std::cout << "Value: " << x << std::endl;
    // x++; // 编译错误:不能修改常量引用的值
}

int main() {
    int a = 10;
    print_value(a); // 通过常量引用将 a 传递给函数 print_value

    return 0;
}

注意事项:
引用具有以下特点和限制:

引用必须在定义时初始化,且在其生命周期内不能重新指向其他变量。
引用不占用额外的存储空间,它只是为变量创建了一个别名。
引用可以作为函数参数和返回值,但要确保引用的生命周期在函数返回后仍然有效。
常量引用不能修改引用的变量值。
综上所述,引用是 C++ 中的一种强大且灵活的特性。在使用时,要注意引用的生命周期和作用范围,以确保代码的正确性。

六、C++内存管理

(一)动态内存分配

在 C++ 中,动态内存分配是指在程序运行过程中根据需要来分配和释放内存。动态内存分配可以在程序运行时动态地创建对象,数组等,使得程序能够更灵活地处理不同大小的数据。使用动态内存分配的关键在于对内存的申请、使用和释放进行合理管理。

1、动态分配内存:

在 C++ 中,可以使用 new 操作符来动态分配内存。new 会在堆(heap)上分配一块合适的内存空间,并返回一个指向该空间的指针。

int *p = new int; // 分配一个 int 类型的内存空间,并返回指针 p

也可以使用 new 分配一个数组:

int *arr = new int[10]; // 分配一个大小为 10 的 int 类型数组,并返回指针 arr

2、使用动态分配的内存:

使用动态分配的内存与使用普通变量或数组相似,只需使用指针访问所分配的内存空间即可。

*p = 42; // 给动态分配的 int 变量赋值
arr[5] = 10; // 给动态分配的数组中的某个元素赋值

3、释放动态分配的内存:

在使用完动态分配的内存后,需要使用 delete 操作符来释放内存。对于分配单个变量的内存,使用 delete 释放:

delete p; // 释放动态分配的 int 变量的内存

对于分配数组的内存,使用 delete[] 释放:

delete[] arr; // 释放动态分配的 int 数组的内存

注意事项:

动态分配内存后,务必确保在使用完毕后正确释放内存,以避免内存泄漏。
不要重复释放同一块内存,这可能导致未定义行为。
在使用完动态分配的内存后,可以将指针设为 nullptr,以避免悬空指针的产生。
总之,动态内存分配是 C++ 中一种非常有用的技术,它可以让程序在运行时更灵活地处理不同大小的数据。但要注意合理管理动态内存,避免内存泄漏和悬空指针等问题。

(二)内存泄漏与管理

内存泄漏是指程序中已经分配的内存没有被释放,从而导致系统内存资源的浪费。在 C++ 中,由于程序员需要手动管理内存,因此内存泄漏是一个比较常见的问题。为了避免内存泄漏和提高程序的健壮性,需要对内存进行有效管理。

内存泄漏的原因:

动态分配内存后,没有在适当的时候使用 delete 或 delete[] 释放内存。
程序的某个分支忘记释放内存,比如在函数中有多个返回路径,其中一个路径没有释放内存。
释放内存的语句没有执行到,比如由于异常抛出,导致释放内存的语句没有被执行。
使用了已经释放的指针继续分配内存,导致原来的内存无法被释放。
内存管理的方法:

在使用动态分配的内存后,确保在适当的时候使用 delete 或 delete[] 释放内存。
使用智能指针(如 std::unique_ptr、std::shared_ptr 等)来自动管理内存。智能指针在离开作用域时会自动释放所管理的内存,避免了手动管理内存的繁琐和错误。
使用 RAII(资源获取即初始化)原则管理资源。将资源(如内存、文件等)的分配和释放绑定到对象的生命周期上,确保资源在对象销毁时自动释放。
在复杂的程序中,可以使用内存分析工具(如 Valgrind)来检测内存泄漏,以便更早地发现并解决问题。
总之,避免内存泄漏是 C++ 程序员的重要任务之一,通过使用智能指针、RAII 原则和内存分析工具,可以有效地管理内存并提高程序的健壮性。

七、面向对象编程

(一)类与对象

类和对象是面向对象编程(OOP)的核心概念。在 C++ 中,类是一种自定义数据类型,它封装了数据(成员变量)和操作这些数据的方法(成员函数)。对象则是类的实例,它是程序运行时分配的内存空间,用于存储类的数据成员并执行成员函数。

1、类的定义:

类的定义使用关键字 class,后跟类名。类的成员变量和成员函数分别声明在类的大括号内,通常成员变量声明在前,成员函数声明在后。类的成员可以具有不同的访问修饰符:public、private 和 protected,分别表示公有、私有和受保护的成员。

class MyClass {
public:
    // 公有成员变量和成员函数
private:
    // 私有成员变量和成员函数
protected:
    // 受保护成员变量和成员函数
};

2、对象的创建:

创建类的对象时,可以在栈上创建,也可以在堆上创建。在栈上创建对象时,对象的生命周期由系统自动管理;在堆上创建对象时,需要使用 new 关键字动态分配内存,并在使用完后使用 delete 关键字释放内存。

MyClass obj1; // 在栈上创建对象
MyClass* obj2 = new MyClass(); // 在堆上创建对象
delete obj2; // 释放堆上对象的内存

3、成员函数的调用:

通过对象或对象指针调用成员函数,可以使用 . 或 -> 运算符。

obj1.myFunction(); // 使用对象调用成员函数
obj2->myFunction(); // 使用对象指针调用成员函数

总结一下,类是 C++ 中自定义数据类型的基础,用于封装数据和操作数据的方法。对象是类的实例,用于存储数据并执行成员函数。通过类和对象,可以更好地实现面向对象编程的特性,如封装、继承和多态。

(二)构造函数与析构函数

构造函数和析构函数是类的特殊成员函数,它们在对象的生命周期的开始和结束时被自动调用。它们的主要目的是对对象进行初始化和清理工作。

1、构造函数:

构造函数用于初始化对象的成员变量。它的名称与类名相同,没有返回值类型。
构造函数可以有参数,也可以没有参数。可以为构造函数提供默认参数。
如果没有为类定义任何构造函数,编译器会自动生成一个默认构造函数。默认构造函数没有参数,且不执行任何操作。
构造函数可以被重载,即可以有多个具有不同参数的构造函数。
当创建类的对象时,会自动调用与实参匹配的构造函数进行对象的初始化。

class MyClass {
public:
    MyClass() { // 无参数的构造函数
        // 初始化操作
    }

    MyClass(int a) { // 有参数的构造函数
        // 初始化操作
    }
};

MyClass obj1; // 调用无参数的构造函数
MyClass obj2(10); // 调用有参数的构造函数

2、析构函数:

析构函数用于清理对象占用的资源,如动态分配的内存或打开的文件等。它的名称与类名相同,但前面加上了波浪符(~),没有返回值类型且不能有参数。
一个类只能有一个析构函数。
当对象的生命周期结束时(如离开作用域或使用 delete 关键字释放堆上的对象),析构函数会自动被调用。
如果没有为类定义析构函数,编译器会自动生成一个默认析构函数。默认析构函数不执行任何操作。

class MyClass {
public:
    ~MyClass() { // 析构函数
        // 清理操作
    }
};

{
    MyClass obj; // 创建对象
    // 当离开作用域时,析构函数自动调用
}

总结一下,构造函数和析构函数是类的特殊成员函数,分别在对象的生命周期开始和结束时自动调用。构造函数用于初始化对象的成员变量,而析构函数用于清理对象占用的资源。

(三)类成员与访问控制

在C++中,类的成员分为数据成员和成员函数。数据成员用于存储类的状态,成员函数用于操作这些状态。为了保护类的数据成员免受外部访问和不恰当的修改,C++提供了访问控制机制。访问控制主要通过访问修饰符来实现,它们包括public、private和protected。

public(公有):

在类外部和类内部都可以访问public成员。
通常将类的接口放在public区域,以便用户能够访问这些成员。
private(私有):

只能在类内部访问private成员,类的外部无法访问。
通常将类的实现细节和敏感数据放在private区域,以保护类的内部状态。
protected(受保护):

在类内部和派生类(子类)中可以访问protected成员,但在类外部无法访问。
当需要在子类中访问基类(父类)的成员时,可以使用protected。
示例:

class MyClass {
public:
    // 公有成员
    void publicFunction() {
        // 公有成员可以在类内外访问
    }

private:
    // 私有成员
    int privateData;
    void privateFunction() {
        // 私有成员只能在类内部访问
    }

protected:
    // 受保护成员
    int protectedData;
    void protectedFunction() {
        // 受保护成员可以在类内部和派生类中访问
    }
};

MyClass obj;
obj.publicFunction(); // 可以访问
// obj.privateFunction(); // 错误,无法访问
// obj.protectedFunction(); // 错误,无法访问

总之,访问修饰符用于限制类成员的访问范围,以保护类的内部状态。public成员可以在类内外访问,private成员只能在类内部访问,而protected成员可以在类内部和派生类中访问。这有助于实现封装和继承,是面向对象编程的重要概念。

(四)类的继承

类的继承是面向对象编程的一个核心概念,它允许在现有类的基础上创建新类,从而实现代码的重用和模块化。在C++中,继承通过派生类(子类)从基类(父类)中继承属性和行为来实现。

基类(父类): 现有的类,包含通用属性和方法。基类可以有多个派生类。
派生类(子类): 从基类继承属性和方法的新类。派生类可以继承一个或多个基类,并扩展或覆盖基类的属性和方法。
继承的语法:

class DerivedClass : access_specifier BaseClass {
    // 派生类的成员和方法
};

其中,access_specifier可以是public、protected或private,用于控制基类成员在派生类中的访问权限。

示例:

// 基类
class Animal {
public:
    void makeSound() {
        //...
    }
};

// 派生类
class Dog : public Animal {
public:
    void bark() {
        //...
    }
};

int main() {
    Dog dog;
    dog.makeSound(); // 调用从基类继承的方法
    dog.bark();      // 调用派生类自己的方法
    return 0;
}

注意事项:

基类的构造函数不会被继承,但会在派生类对象创建时自动调用。派生类可以通过成员初始化列表显式调用基类的构造函数。
如果基类有虚析构函数,派生类会自动继承它。当删除一个指向派生类对象的基类指针时,虚析构函数确保派生类的析构函数被调用。
继承可以实现多态。多态允许使用基类指针或引用来操作派生类对象,从而实现不同类型对象的通用处理。为实现多态,需要在基类中声明虚函数,派生类可以覆盖这些虚函数以提供自己的实现。
继承是面向对象编程的重要特性,有助于实现代码重用、模块化和多态。

(五)多态与虚函数

多态是面向对象编程的一个重要特性,它允许在一个基类指针或引用上调用派生类的方法。多态有助于编写可扩展和可维护的代码,因为你可以编写一段通用的代码来处理基类及其派生类的对象。

在C++中,要实现多态,需要使用虚函数。虚函数是在基类中声明的,可以在派生类中被覆盖。虚函数的调用是在运行时动态解析的,而非在编译时静态解析的。这意味着编译器会根据对象的实际类型选择正确的虚函数实现。

以下是一个多态和虚函数的例子:

#include <iostream>

// 基类
class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing a shape" << std::endl;
    }
};

// 派生类
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a square" << std::endl;
    }
};

void drawShape(const Shape& shape) {
    shape.draw(); // 多态调用:根据实际对象类型调用相应的 draw() 方法
}

int main() {
    Circle circle;
    Square square;

    drawShape(circle); // 输出 "Drawing a circle"
    drawShape(square); // 输出 "Drawing a square"

    return 0;
}

注意事项:

为实现多态,需要在基类中使用virtual关键字声明函数,这表明该函数可以在派生类中被覆盖。
覆盖虚函数时,派生类中的函数原型应与基类中的函数原型相匹配。从C++11起,可以在派生类中使用override关键字显式指示函数覆盖,以便编译器检查是否正确覆盖了基类的虚函数。
虚函数调用的运行时开销通常由虚函数表(vtable)来实现。虚函数表是一个存储类的虚函数地址的表格,每个类实例都有一个指向虚函数表的指针。当调用虚函数时,编译器会根据实际对象的虚函数表来查找并调用相应的函数实现。
为避免资源泄漏,应确保基类的析构函数是虚函数。这样,在删除一个指向派生类对象的基类指针时,会正确调用派生类的析构函数。
多态和虚函数在C++中非常重要,它们为处理各种对象类型提供了一种通用的方法,有助于提高代码的可维护性和可扩展性。

(六)抽象类与接口

抽象类和接口是面向对象编程中两个重要概念,它们都用于定义对象的公共接口和行为。这些定义可以在派生类中实现,从而达到代码复用和扩展性的目的。

抽象类

抽象类是不能实例化的类,它用于表示一个通用概念或者为其他类提供基本功能。抽象类可以包含实现的成员函数(普通成员函数)和没有实现的成员函数(纯虚函数)。
在C++中,抽象类通过包含至少一个纯虚函数来定义。纯虚函数在基类中没有实现,需要在派生类中被覆盖和实现。纯虚函数的声明形式如下:virtual ReturnType functionName(Parameters…) = 0;
从抽象类派生出的类必须实现所有纯虚函数,否则该派生类仍然是抽象类,不能实例化。

接口:

在C++中,并没有明确的“接口”关键字。但是,我们可以将一个完全由纯虚函数组成的类看作是接口。这样的接口类定义了一组公共方法,派生类可以实现这些方法,以满足接口的规范。
由于接口类只包含纯虚函数,所以它也是一种特殊的抽象类。这意味着接口不能实例化,只能通过派生类来实现接口中的方法。
C++支持多重继承,这意味着一个类可以实现多个接口。要实现多个接口,只需在派生类的声明中列出所有基类,并用逗号分隔。
以下是一个简单的抽象类和接口的例子:

#include <iostream>

// 抽象类
class Animal {
public:
    virtual void makeSound() const = 0; // 纯虚函数
};

// 接口
class Runnable {
public:
    virtual void run() const = 0; // 纯虚函数
};

// 派生类实现抽象类和接口
class Dog : public Animal, public Runnable {
public:
    void makeSound() const override {
        std::cout << "Woof!" << std::endl;
    }

    void run() const override {
        std::cout << "The dog is running." << std::endl;
    }
};

int main() {
    Dog dog;
    dog.makeSound(); // 输出 "Woof!"
    dog.run();       // 输出 "The dog is running."

    return 0;
}

在这个例子中,Animal是一个抽象类,包含一个纯虚函数makeSound()。Runnable是一个接口,包含一个纯虚函数run()。Dog类从Animal和Runnable派生,并实现了它们的纯虚函数。

(七)运算符重载

运算符重载是 C++ 的一种特性,允许为自定义类型(通常是类)重新定义运算符的行为。这可以让我们以更自然的方式使用自定义类型,使其表现得更像内置类型。

要重载一个运算符,需要定义一个与该运算符相关联的函数。这个函数可以是类的成员函数,也可以是全局函数。运算符重载函数的名称由关键字 operator 和要重载的运算符组成,例如 operator+、operator- 等。重载运算符的参数取决于运算符的操作数。

以下是一些运算符重载的例子:

#include <iostream>

class Complex {
private:
    double real;
    double imag;

public:
    Complex(double r, double i) : real(r), imag(i) {}

    // 重载 + 运算符
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 重载 << 运算符(全局函数)
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};

// 重载 << 运算符的实现
std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;
}

int main() {
    Complex a(3, 4);
    Complex b(1, 2);
    Complex c = a + b;

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
    std::cout << "c = a + b = " << c << std::endl;

    return 0;
}

在这个例子中,我们定义了一个表示复数的 Complex 类。我们重载了 + 运算符,使其可以对复数进行加法运算。我们还重载了 << 运算符,使其可以将复数对象输出到 std::ostream。

需要注意的是,虽然运算符重载提高了代码的可读性,但如果滥用可能导致代码难以理解。在重载运算符时,请确保其行为与内置类型的行为一致,并且易于理解。

(八)友元函数与友元类

友元函数与友元类是 C++ 中一种特殊的访问控制机制,允许一个函数或者类访问另一个类的私有(private)和保护(protected)成员。友元提供了一种绕过封装的方法,但在需要的情况下,它可以帮助我们编写更简洁、更高效的代码。

友元函数
友元函数是一个非成员函数,但可以访问其所在类的私有和保护成员。要声明一个友元函数,只需在类定义中使用 friend 关键字,后面跟函数原型。例如:

class A {
private:
    int x;

public:
    A(int x) : x(x) {}

    // 声明一个友元函数
    friend void printX(const A& a);
};

// 友元函数的实现
void printX(const A& a) {
    std::cout << "A.x = " << a.x << std::endl;
}

在这个例子中,printX 函数被声明为类 A 的友元,因此它可以访问类 A 的私有成员 x。

友元类
当一个类需要访问另一个类的私有和保护成员时,我们可以将其声明为友元类。要声明友元类,只需在类定义中使用 friend 关键字,后面跟类名。例如:

class B;

class A {
private:
    int x;

public:
    A(int x) : x(x) {}

    // 声明一个友元类
    friend class B;
};

class B {
public:
    void printX(const A& a) {
        std::cout << "A.x = " << a.x << std::endl;
    }
};

在这个例子中,类 B 被声明为类 A 的友元,因此它可以访问类 A 的私有成员 x。

需要注意的是,友元关系不具有传递性,也不具有对称性。例如,如果类 A 是类 B 的友元,那么类 B 不一定是类 A 的友元。同样,如果类 A 是类 B 的友元,并且类 B 是类 C 的友元,那么类 A 不一定是类 C 的友元。在使用友元时,应该谨慎对待,避免滥用,以免破坏类的封装性。

八、模板

(一)函数模板

函数模板是 C++ 中一种泛型编程技术,它允许我们编写能够处理不同数据类型的通用函数。函数模板在编译时根据所需的数据类型生成特定的函数实例。这样,我们可以使用一种通用的方式来编写函数,而不需要为每种数据类型重复编写相同的代码。

以下是一个简单的函数模板示例,用于计算两个值的最大值:

#include <iostream>

// 函数模板的定义
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // 使用函数模板实例化并调用 max 函数
    int int_a = 3, int_b = 4;
    double double_a = 3.1, double_b = 2.8;

    std::cout << "Max of integers: " << max(int_a, int_b) << std::endl;        // 输出:Max of integers: 4
    std::cout << "Max of doubles: " << max(double_a, double_b) << std::endl;  // 输出:Max of doubles: 3.1

    return 0;
}

在这个例子中,我们定义了一个名为 max 的函数模板,它接受两个参数 a 和 b,并返回它们的最大值。template 表示这是一个函数模板,其中 T 是一个类型参数,它可以表示任何数据类型。在编译时,编译器会根据实际使用的数据类型生成相应的函数实例。

当我们调用 max(int_a, int_b) 时,编译器会自动推导出 T 应该是 int 类型,然后为 int 类型生成一个 max 函数实例。同样,当我们调用 max(double_a, double_b) 时,编译器会自动推导出 T 应该是 double 类型,并为 double 类型生成一个 max 函数实例。

函数模板可以帮助我们编写更加灵活、通用的代码,减少代码重复,并提高代码的可维护性。

(二)类模板

类模板是 C++ 中的泛型编程技术,它允许我们定义能够处理不同数据类型的通用类。类模板在编译时根据所需的数据类型生成特定的类实例。这样,我们可以使用一种通用的方式来定义类,而不需要为每种数据类型重复编写相同的代码。

以下是一个简单的类模板示例,用于实现一个简单的存储器:

#include <iostream>

// 类模板的定义
template <typename T>
class Storage {
public:
    Storage(T value) : value_(value) {}

    void setValue(T value) {
        value_ = value;
    }

    T getValue() const {
        return value_;
    }

private:
    T value_;
};

int main() {
    // 使用类模板实例化并创建对象
    Storage<int> intStorage(10);
    Storage<double> doubleStorage(3.14);

    std::cout << "Integer storage: " << intStorage.getValue() << std::endl;       // 输出:Integer storage: 10
    std::cout << "Double storage: " << doubleStorage.getValue() << std::endl;     // 输出:Double storage: 3.14

    return 0;
}

在这个例子中,我们定义了一个名为 Storage 的类模板,它接受一个类型参数 T,并包含一个 T 类型的私有成员变量 value_。类模板的成员函数 setValue 和 getValue 分别用于设置和获取 value_ 的值。template 表示这是一个类模板,其中 T 是一个类型参数,可以表示任何数据类型。

在 main 函数中,我们使用类模板实例化了两个 Storage 类的对象,分别用于存储 int 和 double 类型的数据。编译器会根据实际使用的数据类型生成相应的类实例。

类模板可以帮助我们编写更加灵活、通用的代码,减少代码重复,并提高代码的可维护性。

(三)模板特化与偏特化

模板特化与偏特化是 C++ 中模板编程的高级技巧,它们用于为特定的类型或类型组合提供特殊的模板实现。这有助于优化代码,提供特定类型的高效实现,或处理某些类型的特殊情况。

模板特化(Template Specialization):
模板特化是针对模板中的特定类型参数提供一种特殊的实现。当编译器在实例化模板时遇到这些特定类型时,它将使用特化版本的实现,而不是通用模板。特化通常通过在模板定义前加上 template <> 关键字并提供特定类型参数来实现。

例如,假设我们有一个通用模板,用于计算两个数的最大值:

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

我们可以为 char 类型提供一个特化版本,以便在比较字符时不区分大小写:

template <>
char max<char>(char a, char b) {
    return (tolower(a) > tolower(b)) ? a : b;
}

偏特化(Partial Template Specialization):
偏特化适用于类模板,它允许我们为模板的部分类型参数提供特殊实现。在偏特化中,我们可以为特定类型组合提供自定义实现,而不必完全特化每个类型参数。这可以通过在模板定义前加上 template <typename…> 关键字并提供部分特定类型参数来实现。

例如,假设我们有一个通用的 Pair 类模板:

template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {}

    // ...
private:
    T1 first_;
    T2 second_;
};

我们可以为两个相同类型参数的 Pair 提供一个偏特化版本,以便在这种情况下使用更高效的实现:

template <typename T>
class Pair<T, T> {
public:
    Pair(T first, T second) : first_(first), second_(second) {}

    // ...
private:
    T first_;
    T second_;
};

总之,模板特化和偏特化是 C++ 模板编程的重要技巧,它们使我们能够为特定类型或类型组合提供自定义实现,以优化代码性能或处理特殊情况。

九、异常处理

(一)try-catch语句

try-catch 语句是 C++ 中用于处理异常(异常是程序执行过程中出现的意外情况)的一种机制。通过使用 try-catch 语句,我们可以将可能引发异常的代码放在 try 块中,并在 catch 块中处理异常。

当程序执行到 try 块时,如果在 try 块中的代码没有发生异常,则会跳过 catch 块,继续执行后续代码。但是,如果在 try 块中的代码发生了异常,程序会立即跳出 try 块,进入与抛出异常类型匹配的 catch 块进行异常处理。

下面是一个简单的 try-catch 示例:

#include <iostream>

int main() {
    int a = 10;
    int b = 0;
    int result;

    try {
        if (b == 0) {
            throw std::runtime_error("Division by zero");
        }
        result = a / b;
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中,我们尝试执行除法操作。当分母 b 为 0 时,我们抛出一个 std::runtime_error 类型的异常。catch 块捕获异常并输出错误信息。

请注意,可以在一个 try 语句后面添加多个 catch 块来处理不同类型的异常。在这种情况下,程序会根据抛出的异常类型选择相应的 catch 块。这是一个处理多种异常的示例:

#include <iostream>

int main() {
    int a = 10;
    int b = 0;
    int result;

    try {
        if (b == 0) {
            throw std::runtime_error("Division by zero");
        }
        result = a / b;
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "General exception: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中,我们添加了一个处理 std::exception 类型的通用异常处理 catch 块。如果抛出的异常是 std::runtime_error 类型,则程序会进入第一个 catch 块;如果抛出的异常是其他类型的 std::exception,则会进入第二个 catch 块。

(二)异常类与自定义异常

C++ 中的异常类通常继承自 std::exception 类,该类是 C++ 标准库中所有异常类的基类。std::exception 类定义了一个名为 what() 的虚函数,该函数可以返回一个描述异常原因的字符串。我们可以通过继承 std::exception 并重写 what() 函数来创建自定义异常类。

以下是一个自定义异常类的示例:

#include <iostream>
#include <stdexcept>
#include <string>

class DivisionByZeroException : public std::runtime_error {
public:
    DivisionByZeroException()
        : std::runtime_error("Division by zero") {}
};

int main() {
    int a = 10;
    int b = 0;
    int result;

    try {
        if (b == 0) {
            throw DivisionByZeroException();
        }
        result = a / b;
        std::cout << "Result: " << result << std::endl;
    } catch (const DivisionByZeroException& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "General exception: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中,我们创建了一个名为 DivisionByZeroException 的自定义异常类,该类继承自 std::runtime_error。由于 std::runtime_error 本身就是从 std::exception 继承的,我们的自定义异常类也间接地继承自 std::exception。

DivisionByZeroException 构造函数调用了基类 std::runtime_error 的构造函数,将异常描述信息传递给基类。我们没有重写 what() 函数,因为基类 std::runtime_error 已经提供了一个合适的实现。

在 main() 函数中,我们使用 try-catch 语句处理自定义异常。当 b 为 0 时,我们抛出一个 DivisionByZeroException 类型的异常。catch 块捕获异常并输出错误信息。

(三)标准异常库

C++ 标准库提供了一组预定义的异常类,它们可以用于处理常见的异常情况。这些异常类继承自 std::exception 类,它是所有标准异常类的基类。std::exception 类定义了一个名为 what() 的虚函数,该函数返回一个描述异常原因的字符串。

以下是一些常用的标准异常类:

std::logic_error:表示程序逻辑错误的异常类。这类异常通常是由于程序错误导致的,例如使用了无效的参数或在不允许的情况下调用了函数。

std::invalid_argument:表示给函数传递了无效参数的异常类。
std::domain_error:表示数学函数的参数超出了函数定义域的异常类。
std::length_error:表示创建的对象长度超出了最大允许长度的异常类。
std::out_of_range:表示访问序列中不存在的元素的异常类。
std::runtime_error:表示在运行时检测到的错误的异常类。这类异常通常是由于外部因素导致的,例如资源不足或系统调用失败。

std::range_error:表示数值运算的结果超出了表示范围的异常类。
std::overflow_error:表示算术运算导致的溢出的异常类。
std::underflow_error:表示算术运算导致的下溢的异常类。
std::bad_alloc:表示动态内存分配失败(例如 new 操作符)的异常类。这通常是因为系统内存不足导致的。

std::bad_cast:表示 dynamic_cast 失败的异常类。当尝试将引用或指针转换为不兼容类型时,将抛出此异常。

std::bad_function_call:表示调用了空的 std::function 对象的异常类。

std::bad_weak_ptr:表示构造 std::shared_ptr 时传递了无效的 std::weak_ptr 的异常类。

这些标准异常类提供了一种统一、可预测的方式来处理各种异常情况。在编写自定义异常类时,可以根据需要从这些标准异常类继承,并添加自定义的错误信息和行为。

十、标准库

(一)STL(Standard Template Library)

STL(Standard Template Library,标准模板库)是 C++ 标准库的一个重要组成部分,它提供了一组丰富的数据结构和算法。STL 的主要目标是实现通用、高效、易用的组件。STL 采用模板的方式实现,可以在编译时进行类型检查和优化,从而提高程序的运行效率。

STL 主要包含以下几个组件:

容器(Containers):容器是用于存储和管理数据的数据结构。STL 提供了一系列容器,如向量(vector)、列表(list)、双端队列(deque)、集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)等。容器可以分为序列容器(如 vector、list、deque)、关联容器(如 set、multiset、map、multimap)和无序关联容器(如 unordered_set、unordered_multiset、unordered_map、unordered_multimap)。

迭代器(Iterators):迭代器是一种类似于指针的对象,用于访问容器中的元素。迭代器提供了一种通用的访问机制,使得可以用相同的方式访问各种不同类型的容器。迭代器可以分为五种类型:输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。

算法(Algorithms):STL 提供了一系列通用算法,如查找、排序、复制、删除等。这些算法可以应用于不同类型的容器,实现了算法和数据结构之间的解耦。大部分 STL 算法接受迭代器作为参数,这样可以方便地对容器中的元素进行操作。

函数对象(Function Objects):函数对象(也称为仿函数)是一种重载了函数调用运算符 operator() 的类对象。函数对象可以像普通函数一样调用,但具有状态和可重用性。STL 提供了一些预定义的函数对象,如算术运算(plus、minus、multiplies、divides、modulus)、关系运算(equal_to、not_equal_to、greater、less、greater_equal、less_equal)和逻辑运算(logical_and、logical_or、logical_not)等。此外,用户可以定义自己的函数对象,以满足特定需求。

适配器(Adapters):适配器是一种将一个容器、迭代器或函数对象转换为另一种接口的组件。STL 提供了几种容器适配器,如栈(stack)、队列(queue)和优先级队列(priority_queue)等。此外,还有迭代器适配器(如插入迭代器、流迭代器和逆向迭代器等)和函数对象适配器(如绑定器、否定器和函数指针适配器等)。

STL 的优点在于它提供了高度模块化的组件,这些组件可以组合在一起以实现复杂的功能。这些组件也是可重用的,可以在多个项目中使用,从而提高了代码的复用性。此外,STL 的泛型编程特性允许在编译时进行类型检查和优化,从而提高了程序的运行效率。

使用 STL 时,需要包含相应的头文件。例如:

#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <functional>
#include <iterator>

在实际编程中,通过熟练掌握 STL 的各个组件,可以大大提高编程效率和代码质量。这需要不断地学习和实践,以便更好地理解和运用 STL 提供的丰富功能。

(二)容器(vector、list、deque、set、map等)

C++ STL 容器是一组类模板,用于存储和管理数据。它们提供了许多预定义的数据结构,如向量、链表、集合、映射等。这些容器大致分为三类:顺序容器、关联容器和容器适配器。

1、顺序容器:

顺序容器按照线性顺序存储元素。主要顺序容器有:

std::vector:动态数组,支持随机访问,快速添加/删除元素到尾部,但在中间添加/删除元素较慢。
std::list:双向链表,支持快速添加/删除元素到任意位置,但不支持随机访问。
std::deque:双端队列,支持随机访问,快速添加/删除元素到头部和尾部。

2、关联容器:

关联容器是基于键值对存储的数据结构。主要关联容器有:

std::set:元素按照排序顺序存储,元素不重复。支持高效查找、删除和插入操作。
std::multiset:与 std::set 类似,但允许重复的元素。
std::map:基于键值对的关联数组,其中键是唯一的。支持高效查找、删除和插入操作。
std::multimap:与 std::map 类似,但允许具有相同键的多个元素。
C++11 引入了无序关联容器:

std::unordered_set:基于哈希表的集合,元素不重复。支持高效查找、删除和插入操作。
std::unordered_multiset:与 std::unordered_set 类似,但允许重复的元素。
std::unordered_map:基于哈希表的键值对关联数组,其中键是唯一的。支持高效查找、删除和插入操作。
std::unordered_multimap:与 std::unordered_map 类似,但允许具有相同键的多个元素。

3、容器适配器:

容器适配器是在现有容器的基础上,提供特定接口的容器。主要容器适配器有:

std::stack:后进先出(LIFO)的栈容器。
std::queue:先进先出(FIFO)的队列容器。
std::priority_queue:基于优先级的队列容器,优先级最高的元素总是在队列的顶部。
使用这些容器可以方便地实现各种数据结构和算法,提高代码的可读性和可维护性。要使用这些容器,需要在代码中包含相应的头文件

(三)迭代器

迭代器是一种类似于指针的对象,用于遍历容器中的元素。STL 中的每个容器都提供了迭代器,用于访问和操作元素。主要的迭代器类型有:

输入迭代器:只允许在容器中向前移动,支持读取元素。
输出迭代器:只允许在容器中向前移动,支持写入元素。
前向迭代器:在容器中向前移动,支持读取和写入元素。
双向迭代器:在容器中向前和向后移动,支持读取和写入元素。
随机访问迭代器:支持在容器中任意移动,支持读取和写入元素。

(四)算法

STL 提供了许多通用算法,用于对容器中的元素进行操作。这些算法包括排序、查找、复制、删除等。要使用这些算法,需要包含 头文件。一些常用的算法包括:

std::sort:对指定范围内的元素进行排序。
std::find:在指定范围内查找给定的元素。
std::copy:将指定范围内的元素复制到另一个位置。
std::remove:从指定范围内删除给定的元素。

(五)输入输出流库

C++ 的输入输出流库(IO库)提供了对输入输出设备的访问和控制。它是 C++ 标准库的一部分,通过包含头文件 来使用。输入输出流库基于流(stream)的概念,流是一个抽象的数据通道,用于在内存和输入输出设备(如键盘、显示器、文件等)之间传输数据。

IO库包含以下主要组件:

标准流对象:
IO库提供了一些预定义的流对象,用于处理标准输入、标准输出和标准错误:

std::cin:表示标准输入流(通常是键盘),用于从输入设备读取数据。
std::cout:表示标准输出流(通常是显示器),用于将数据输出到输出设备。
std::cerr:表示标准错误流,用于输出错误信息。
std::clog:表示标准日志流,用于输出日志信息。
这些流对象都属于命名空间 std。

输入输出操作符:
IO库为输入输出流定义了一组操作符,用于从流中读取数据和向流中写入数据:

<<:插入符(输出操作符),用于向输出流中写入数据。例如:std::cout << “Hello, World!” << std::endl;

:提取符(输入操作符),用于从输入流中读取数据。例如:int age; std::cin >> age;
流操作:
IO库提供了一些用于操作流的函数和操作符,例如:

std::endl:表示换行符,用于在输出流中插入换行符并刷新缓冲区。
std::flush:用于刷新输出流的缓冲区,将缓冲区中的数据立即发送到输出设备。
std::ws:从输入流中提取并丢弃空白字符(空格、制表符和换行符)。
文件流:
C++ 的输入输出流库还包括对文件操作的支持。通过包含头文件 来使用。文件流对象允许从文件中读取数据和向文件中写入数据。主要的文件流类有:

std::ifstream:用于从文件中读取数据的输入文件流类。
std::ofstream:用于向文件中写入数据的输出文件流类。
std::fstream:用于同时进行输入输出操作的文件流类。
这里是一个简单的文件读写示例:

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream outFile("example.txt");
    if (outFile.is_open()) {
        outFile << "Hello, World!" << std::endl;
        outFile.close();
    } else {
        std::cerr << "Unable to open file for writing." << std::endl;
    }

    std::ifstream inFile("example.txt");
    if (inFile.is_open()) {
        std::string line;
        while (std::getline(inFile, line)) {
            std::cout << line << std::endl;
        }
        inFile.close();
    } else {
        std::cerr << "Unable to open file for reading." << std::endl;
    }


    return 0;
}

在这个示例中,我们首先创建一个名为 example.txt 的文件并向其中写入一行文本。然后,我们打开相同的文件并逐行读取其内容并输出到屏幕上。

总结一下,C++ 的输入输出流库提供了对输入输出设备的访问和控制,支持标准输入输出流、文件流以及相关操作符和操作。通过

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值