恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

零基础C++(17) 语句和作用域

Posted on 2024-10-27 | In 零基础C++

1. 简单语句

1.1 表达式语句

描述:在 C++ 中,最常见的简单语句是表达式语句。它由一个表达式组成,并以分号结束。表达式语句可以包括函数调用、赋值操作、增减操作等。

示例代码:

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
int a = 5; // 赋值表达式语句
a = a + 10; // 赋值表达式语句
std::cout << a; // 函数调用表达式语句
return 0; // return 表达式语句
}

讲解:

  • int a = 5; 初始化变量 a,这是一个赋值表达式语句。
  • a = a + 10; 更新变量 a 的值。
  • std::cout << a; 调用了输出流对象的 << 运算符函数。
  • return 0; 从 main 函数返回,结束程序。

1.2 声明语句

描述:声明语句用于声明变量、函数、类等标识符。

示例代码:

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

// 函数声明
int add(int x, int y);

int main() {
int result = add(3, 4); // 调用函数
std::cout << "Result: " << result;
return 0;
}

// 函数定义
int add(int x, int y) {
return x + y;
}

讲解:

  • int add(int x, int y); 是一个函数声明语句。
  • int result = add(3, 4); 在 main 中调用函数并声明变量。
  • int add(int x, int y) { ... } 是函数定义,提供了函数的实现。

2. 语句作用域

2.1 作用域的基本概念

描述:作用域定义了变量或其他标识符在程序中的可见范围。C++ 中主要有以下几种作用域:

  • **局部作用域 (Local Scope)**:在函数或代码块内部定义的变量,仅在其所在的块内可见。
  • **全局作用域 (Global Scope)**:在所有函数外部定义的变量,在整个文件中可见。
  • **命名空间作用域 (Namespace Scope)**:在命名空间内部定义的标识符。

2.2 代码示例

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

// 全局变量
int globalVar = 10;

void display() {
// 局部变量
int localVar = 5;
std::cout << "Inside display() - globalVar: " << globalVar << ", localVar: " << localVar << std::endl;
}

int main() {
std::cout << "In main() - globalVar: " << globalVar << std::endl;
// 局部变量
int mainVar = 20;
std::cout << "In main() - mainVar: " << mainVar << std::endl;

display();

// 尝试访问 display() 中的局部变量(将导致编译错误)
// std::cout << localVar; // 错误:未定义标识符

return 0;
}

预期输出:

1
2
3
In main() - globalVar: 10
In main() - mainVar: 20
Inside display() - globalVar: 10, localVar: 5

讲解:

  • globalVar 在所有函数中都可见。
  • mainVar 仅在 main 函数内部可见。
  • localVar 仅在 display 函数内部可见。
  • 尝试在 main 中访问 display 函数的 localVar 将导致编译错误,因为它不在作用域内。

2.3 代码块作用域

描述:通过使用花括号 {},可以创建新的代码块,从而定义局部作用域。

示例代码:

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

int main() {
int x = 10;
std::cout << "x before block: " << x << std::endl;

{
// 新的代码块
int x = 20; // 局部变量 x,隐藏外部的 x
std::cout << "x inside block: " << x << std::endl;
}

std::cout << "x after block: " << x << std::endl; // 访问外部的 x

return 0;
}

预期输出:

1
2
3
x before block: 10
x inside block: 20
x after block: 10

讲解:

  • 在内部代码块中重新声明了变量 x,该 x 只在代码块内有效,隐藏了外部的 x。
  • 离开代码块后,内部的 x 不再可见,外部的 x 依然有效。

3. 条件语句

C++ 提供了多种条件语句,用于根据不同的条件执行不同的代码块。

3.1 if 语句

描述:if 语句用于在条件为真时执行特定的代码块。

语法:

1
2
3
if (condition) {
// code to execute if condition is true
}

示例代码:

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

int main() {
int number;
std::cout << "Enter a number: ";
std::cin >> number;

if (number > 0) {
std::cout << "The number is positive." << std::endl;
}

return 0;
}

讲解:

  • 用户输入一个数字,如果 number 大于 0,则输出 “The number is positive.”。

3.2 if-else 语句

描述:if-else 语句在条件为假时执行另一个代码块。

语法:

1
2
3
4
5
if (condition) {
// code to execute if condition is true
} else {
// code to execute if condition is false
}

示例代码:

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

int main() {
int number;
std::cout << "Enter a number: ";
std::cin >> number;

if (number % 2 == 0) {
std::cout << number << " is even." << std::endl;
} else {
std::cout << number << " is odd." << std::endl;
}

return 0;
}

讲解:

  • 判断输入的数字是奇数还是偶数,并输出相应的结果。

3.3 else if 语句

描述:else if 允许在多重条件下执行不同的代码块。

语法:

1
2
3
4
5
6
7
if (condition1) {
// code if condition1 is true
} else if (condition2) {
// code if condition1 is false and condition2 is true
} else {
// code if both condition1 and condition2 are false
}

示例代码:

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

int main() {
int score;
std::cout << "Enter your score (0-100): ";
std::cin >> score;

if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else if (score >= 70) {
std::cout << "Grade: C" << std::endl;
} else if (score >= 60) {
std::cout << "Grade: D" << std::endl;
} else {
std::cout << "Grade: F" << std::endl;
}

return 0;
}

讲解:

  • 根据分数范围判断并输出相应的等级。

3.4 switch 语句

描述:switch 语句根据变量的值选择执行的代码块,适用于离散的值。

语法:

1
2
3
4
5
6
7
8
9
10
11
switch (expression) {
case constant1:
// code
break;
case constant2:
// code
break;
// ...
default:
// code
}

示例代码:

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>

int main() {
char grade;
std::cout << "Enter your grade (A, B, C, D, F): ";
std::cin >> grade;

switch (grade) {
case 'A':
std::cout << "Excellent!" << std::endl;
break;
case 'B':
std::cout << "Good!" << std::endl;
break;
case 'C':
std::cout << "Fair!" << std::endl;
break;
case 'D':
std::cout << "Poor!" << std::endl;
break;
case 'F':
std::cout << "Fail!" << std::endl;
break;
default:
std::cout << "Invalid grade." << std::endl;
}

return 0;
}

讲解:

  • 根据输入的字母等级输出相应的评价。
  • break 语句防止代码“掉入”下一个 case 中。

4. 迭代语句

C++ 提供了多种循环结构,用于重复执行代码块。

4.1 for 循环

描述:for 循环用于已知循环次数的情况,结构紧凑。

语法:

1
2
3
for (initialization; condition; increment) {
// code to execute
}

示例代码:

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

int main() {
std::cout << "Counting from 1 to 5:" << std::endl;

for (int i = 1; i <= 5; ++i) {
std::cout << i << " ";
}

std::cout << std::endl;
return 0;
}

预期输出:

1
2
Counting from 1 to 5:
1 2 3 4 5

讲解:

  • 初始化 int i = 1。
  • 条件 i <= 5 为真时执行循环体。
  • 每次循环后执行 ++i,增加 i 的值。
  • 最终输出 1 到 5。

4.2 while 循环

描述:while 循环在循环前判断条件,适合未知循环次数的情况。

语法:

1
2
3
while (condition) {
// code to execute
}

示例代码:

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

int main() {
int count = 1;
std::cout << "Counting from 1 to 5 using while loop:" << std::endl;

while (count <= 5) {
std::cout << count << " ";
++count;
}

std::cout << std::endl;
return 0;
}

预期输出:

1
2
Counting from 1 to 5 using while loop:
1 2 3 4 5

讲解:

  • 初始化 count = 1。
  • 条件 count <= 5 为真时执行循环体。
  • 每次循环后 ++count 增加 count 的值。

4.3 do-while 循环

描述:do-while 循环在循环后判断条件,保证至少执行一次循环体。

语法:

1
2
3
do {
// code to execute
} while (condition);

示例代码:

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

int main() {
int count = 1;
std::cout << "Counting from 1 to 5 using do-while loop:" << std::endl;

do {
std::cout << count << " ";
++count;
} while (count <= 5);

std::cout << std::endl;
return 0;
}

预期输出:

1
2
Counting from 1 to 5 using do-while loop:
1 2 3 4 5

讲解:

  • 无论条件是否为真,do 块中的代码至少执行一次。
  • 在本例中,count 从 1 开始,逐步增加到 5。

4.4 嵌套循环

描述:一个循环内部嵌套另一个循环,常用于多维数据结构的遍历。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
for (int i = 1; i <= 3; ++i) {
std::cout << "Outer loop iteration " << i << ":" << std::endl;
for (int j = 1; j <= 2; ++j) {
std::cout << " Inner loop iteration " << j << std::endl;
}
}
return 0;
}

预期输出:

1
2
3
4
5
6
7
8
9
Outer loop iteration 1:
Inner loop iteration 1
Inner loop iteration 2
Outer loop iteration 2:
Inner loop iteration 1
Inner loop iteration 2
Outer loop iteration 3:
Inner loop iteration 1
Inner loop iteration 2

讲解:

  • 外层 for 循环控制外层迭代次数。
  • 内层 for 循环在每次外层循环中执行,控制内层迭代次数。

5. 跳转语句

跳转语句用于改变程序的执行流。C++ 中主要有 break、continue、return 和 goto。

5.1 break 语句

描述:break 用于立即终止最近的循环或 switch 语句。

示例代码(在循环中使用 break):

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

int main() {
for (int i = 1; i <= 10; ++i) {
if (i == 5) {
break; // 结束循环
}
std::cout << i << " ";
}
std::cout << "\nLoop exited when i == 5." << std::endl;
return 0;
}

预期输出:

1
2
1 2 3 4 
Loop exited when i == 5.

讲解:

  • 当 i 达到 5 时,break 终止循环,停止进一步的迭代。

5.2 continue 语句

描述:continue 用于跳过当前的循环迭代,继续下一次循环。

示例代码:

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

int main() {
std::cout << "Even numbers between 1 and 10:" << std::endl;
for (int i = 1; i <= 10; ++i) {
if (i % 2 != 0) {
continue; // 跳过奇数
}
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}

预期输出:

1
2
Even numbers between 1 and 10:
2 4 6 8 10

讲解:

  • 当 i 是奇数时,continue 跳过本次循环,避免执行 std::cout 语句。
  • 仅输出偶数。

5.3 return 语句

描述:return 用于从函数中返回一个值或结束函数执行。

示例代码:

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

// 函数,返回两个数中的较大者
int max(int a, int b) {
if (a > b) {
return a; // 返回 a,退出函数
}
return b; // 返回 b,退出函数
}

int main() {
int x = 10, y = 20;
std::cout << "The maximum of " << x << " and " << y << " is " << max(x, y) << "." << std::endl;
return 0;
}

预期输出:

1
The maximum of 10 and 20 is 20.

讲解:

  • max 函数根据条件返回较大的数,并退出函数执行。

5.4 goto 语句

描述:goto 允许无条件跳转到程序中指定的标签。虽然 goto 有时能简化代码,但不推荐频繁使用,因为它会使程序流程难以理解和维护。

示例代码:

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

int main() {
int number;
std::cout << "Enter a positive number (negative to quit): ";
std::cin >> number;

if (number < 0) {
goto end; // 跳转到 end 标签,结束程序
}

std::cout << "You entered: " << number << std::endl;

end:
std::cout << "Program ended." << std::endl;
return 0;
}

预期输出(输入为负数):

1
2
Enter a positive number (negative to quit): -5
Program ended.

讲解:

  • 当输入负数时,goto end; 跳转到 end 标签,结束程序。
  • 尽管可以使用 goto,但建议使用更结构化的控制流,如循环和条件语句。

6. 异常处理语句

异常处理用于应对程序运行过程中可能出现的错误情况,确保程序的健壮性和可靠性。

6.1 try, catch, 和 throw 语句

描述:

  • try 块用于包含可能引发异常的代码。
  • throw 用于抛出异常。
  • catch 块用于捕获并处理异常。

基本语法:

1
2
3
4
5
6
7
8
try {
// code that may throw an exception
} catch (ExceptionType1 e1) {
// handler for ExceptionType1
} catch (ExceptionType2 e2) {
// handler for ExceptionType2
}
// ...

示例代码:

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>
#include <stdexcept>

// 函数,计算除法
double divide(double numerator, double denominator) {
if (denominator == 0) {
throw std::invalid_argument("Denominator cannot be zero."); // 抛出异常
}
return numerator / denominator;
}

int main() {
double num, denom;

std::cout << "Enter numerator: ";
std::cin >> num;
std::cout << "Enter denominator: ";
std::cin >> denom;

try {
double result = divide(num, denom);
std::cout << "Result: " << result << std::endl;
} catch (std::invalid_argument &e) { // 捕获 std::invalid_argument 异常
std::cerr << "Error: " << e.what() << std::endl;
}

std::cout << "Program continues after try-catch." << std::endl;
return 0;
}

预期输出:

1
2
3
4
Enter numerator: 10
Enter denominator: 0
Error: Denominator cannot be zero.
Program continues after try-catch.

讲解:

  • divide 函数在分母为零时抛出 std::invalid_argument 异常。
  • try 块尝试执行 divide 函数。
  • 当异常发生时,执行对应的 catch 块,输出错误信息。
  • 程序在异常处理后继续执行,而不会异常终止。

6.2 多重 catch 块

描述:可以为 try 块指定多个 catch 块,以处理不同类型的异常。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <stdexcept>

int main() {
try {
// 模拟不同类型的异常
int choice;
std::cout << "Choose exception to throw (1: bad_alloc, 2: invalid_argument): ";
std::cin >> choice;

if (choice == 1) {
throw std::bad_alloc();
} else if (choice == 2) {
throw std::invalid_argument("Invalid argument provided.");
} else {
std::cout << "No exception thrown." << std::endl;
}
} catch (std::bad_alloc &e) { // 处理 bad_alloc 异常
std::cerr << "Caught std::bad_alloc: " << e.what() << std::endl;
} catch (std::invalid_argument &e) { // 处理 invalid_argument 异常
std::cerr << "Caught std::invalid_argument: " << e.what() << std::endl;
}

std::cout << "Program continues after try-catch." << std::endl;
return 0;
}

预期输出(选择 1):

1
2
3
Choose exception to throw (1: bad_alloc, 2: invalid_argument): 1
Caught std::bad_alloc: std::bad_alloc
Program continues after try-catch.

讲解:

  • 根据用户输入抛出不同类型的异常。
  • 对应的 catch 块分别处理不同的异常类型。
  • 如果未匹配的异常被抛出且没有对应的 catch 块,将导致程序终止(未在此示例中展示)。

6.3 throw 通常位置

描述:throw 语句可以在任何需要引发异常的位置使用,包括函数内部、嵌套调用中等。

示例代码:

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
#include <iostream>
#include <stdexcept>

// 函数,检查数组索引
int getElement(int arr[], int size, int index) {
if (index < 0 || index >= size) {
throw std::out_of_range("Index is out of range."); // 抛出异常
}
return arr[index];
}

int main() {
int myArray[5] = {10, 20, 30, 40, 50};
int index;

std::cout << "Enter array index (0-4): ";
std::cin >> index;

try {
int value = getElement(myArray, 5, index);
std::cout << "Element at index " << index << " is " << value << "." << std::endl;
} catch (std::out_of_range &e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

预期输出(输入为 3):

1
2
Enter array index (0-4): 3
Element at index 3 is 40.

预期输出(输入为 5):

1
2
Enter array index (0-4): 5
Error: Index is out of range.

讲解:

  • getElement 函数检查索引是否有效,如果无效则抛出 std::out_of_range 异常。
  • main 函数中的 try 块调用 getElement,并在 catch 块中处理异常。

6.4 rethrow 异常

描述:可以在 catch 块中使用 throw 语句重新抛出捕获的异常,以便其他部分处理。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <stdexcept>

// 函数,抛出异常
void func1() {
throw std::runtime_error("Error in func1.");
}

// 函数,调用 func1 并重新抛出异常
void func2() {
try {
func1();
} catch (...) { // 捕获所有异常
std::cout << "func2() caught an exception and is rethrowing it." << std::endl;
throw; // 重新抛出当前异常
}
}

int main() {
try {
func2();
} catch (std::exception &e) { // 在 main 中捕获异常
std::cerr << "Main caught: " << e.what() << std::endl;
}
return 0;
}

预期输出:

1
2
func2() caught an exception and is rethrowing it.
Main caught: Error in func1.

讲解:

  • func1 抛出异常。
  • func2 调用 func1,捕获异常后重新抛出。
  • main 最终捕获并处理异常。

练习题

练习题 1:打印九九乘法表

题目描述

编写一个 C++ 程序,使用嵌套的 for 循环来打印标准的九九乘法表。输出的格式应整齐对齐,便于阅读。

要求

  1. 使用嵌套的 for 循环实现。
  2. 输出的乘法表应从 1×1 到 9×9。
  3. 每行输出一个数字的乘法结果,例如第 3 行包含 3×1=3 到 3×9=27。
  4. 确保输出格式整齐,便于阅读。

示例输出

1
2
3
4
5
6
7
8
9
1x1=1	1x2=2	1x3=3	1x4=4	1x5=5	1x6=6	1x7=7	1x8=8	1x9=9	
2x1=2 2x2=4 2x3=6 2x4=8 2x5=10 2x6=12 2x7=14 2x8=16 2x9=18
3x1=3 3x2=6 3x3=9 3x4=12 3x5=15 3x6=18 3x7=21 3x8=24 3x9=27
4x1=4 4x2=8 4x3=12 4x4=16 4x5=20 4x6=24 4x7=28 4x8=32 4x9=36
5x1=5 5x2=10 5x3=15 5x4=20 5x5=25 5x6=30 5x7=35 5x8=40 5x9=45
6x1=6 6x2=12 6x3=18 6x4=24 6x5=30 6x6=36 6x7=42 6x8=48 6x9=54
7x1=7 7x2=14 7x3=21 7x4=28 7x5=35 7x6=42 7x7=49 7x8=56 7x9=63
8x1=8 8x2=16 8x3=24 8x4=32 8x5=40 8x6=48 8x7=56 8x8=64 8x9=72
9x1=9 9x2=18 9x3=27 9x4=36 9x5=45 9x6=54 9x7=63 9x8=72 9x9=81

提示

  • 使用两个嵌套的 for 循环:外层循环控制行数(1 到 9),内层循环控制列数(1 到 9)。
  • 使用 \t 或者适当的空格来对齐输出结果。
  • 可以使用 std::cout 进行输出。

参考答案

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

int main() {
// 外层循环控制行数
for (int i = 1; i <= 9; ++i) {
// 内层循环控制列数
for (int j = 1; j <= 9; ++j) {
std::cout << i << "x" << j << "=" << i * j << "\t";
}
std::cout << std::endl; // 每行结束后换行
}
return 0;
}

练习题 2:实现冒泡排序

题目描述

编写一个 C++ 程序,使用 冒泡排序算法 对用户输入的一组整数进行排序。冒泡排序是一种简单的排序算法,通过重复交换相邻的未按顺序排列的元素,将最大或最小的元素“冒泡”到序列的一端。

要求

  1. 输入:用户输入一组整数,首先输入整数的数量 n,然后输入 n 个整数。
  2. 排序:使用冒泡排序算法对输入的整数进行升序排序。
  3. 输出:显示排序前和排序后的整数序列。
  4. 函数封装:将冒泡排序算法封装在一个独立的函数中,提高代码的模块化和可读性。

示例输入与输出

示例 1:

1
2
3
4
请输入整数的数量: 5
请输入 5 个整数,用空格分隔: 64 34 25 12 22
排序前的数组: 64 34 25 12 22
排序后的数组: 12 22 25 34 64

示例 2:

1
2
3
4
请输入整数的数量: 8
请输入 8 个整数,用空格分隔: 5 1 4 2 8 0 2 9
排序前的数组: 5 1 4 2 8 0 2 9
排序后的数组: 0 1 2 2 4 5 8 9

提示

  • 冒泡排序的基本思想:通过多次遍历数组,每次比较相邻的元素并交换顺序错误的元素。每一轮遍历后,最大的元素会被移动到数组的末端。
  • 优化:如果在某一轮遍历中没有发生任何交换,说明数组已经有序,可以提前终止排序过程。
  • 函数设计:可以设计一个 bubbleSort 函数接收数组及其大小作为参数,并对数组进行排序。

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>

// 冒泡排序函数
void bubbleSort(int arr[], int n) {
bool swapped;
// 外层循环控制总的遍历次数
for (int i = 0; i < n - 1; ++i) {
swapped = false;
// 内层循环进行相邻元素的比较和交换
for (int j = 0; j < n - i - 1; ++j) {
// 如果前一个元素大于后一个元素,则交换它们
if (arr[j] > arr[j + 1]) {
std::swap(arr[j], arr[j + 1]);
swapped = true;
}
}
// 如果在一轮遍历中没有进行任何交换,数组已经有序
if (!swapped) {
break;
}
}
}

// 打印数组函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

int main() {
int n;

// 获取数组大小
std::cout << "请输入整数的数量: ";
std::cin >> n;

if (n <= 0) {
std::cerr << "错误:数组大小必须为正整数。" << std::endl;
return 1;
}

int *arr = new int[n];

// 获取数组元素
std::cout << "请输入 " << n << " 个整数,用空格分隔: ";
for (int i = 0; i < n; ++i) {
std::cin >> arr[i];
}

// 打印排序前的数组
std::cout << "排序前的数组: ";
printArray(arr, n);

// 执行冒泡排序
bubbleSort(arr, n);

// 打印排序后的数组
std::cout << "排序后的数组: ";
printArray(arr, n);

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

return 0;
}

代码解释

  1. **函数 bubbleSort**:

    • 参数:接收一个整数数组 arr 和数组的大小 n。

    • 逻辑

      :

      • 使用两层 for 循环实现冒泡排序。
      • 外层循环控制需要进行的遍历次数,总共需要 n-1 轮。
      • 内层循环进行相邻元素的比较和交换,每一轮内层循环会将当前未排序部分的最大元素移动到数组的末端。
      • 使用 swapped 标志位优化排序过程,如果一轮内层循环中没有发生任何交换,说明数组已经有序,提前终止排序。
    • 交换操作:使用 std::swap 函数交换两个元素的位置。

  2. **函数 printArray**:

    • 功能:遍历数组并打印每个元素,便于观察排序前后的结果。
  3. main 函数:

    • 步骤

      :

      1. 输入数组大小:提示用户输入要排序的整数数量 n。
      2. 输入数组元素:动态分配一个大小为 n 的整数数组,并从用户处获取 n 个整数的输入。
      3. 打印排序前的数组:调用 printArray 函数显示原始数组。
      4. 执行冒泡排序:调用 bubbleSort 函数对数组进行排序。
      5. 打印排序后的数组:再次调用 printArray 函数显示排序后的数组。
      6. 内存管理:使用 delete[] 释放动态分配的内存,避免内存泄漏。
  4. 错误处理:

    • 判断用户输入的数组大小 n 是否为正整数,否者输出错误信息并终止程序。

运行示例

1
2
3
4
请输入整数的数量: 5
请输入 5 个整数,用空格分隔: 64 34 25 12 22
排序前的数组: 64 34 25 12 22
排序后的数组: 12 22 25 34 64

练习题 3:生成斐波那契数列

题目描述

编写一个 C++ 程序,生成并显示斐波那契数列。程序应允许用户指定生成数列的长度,并使用 循环结构 或 递归方法 来生成斐波那契数。

斐波那契数列是一个由 0 和 1 开始,后续的每个数都是前两个数之和的数列。例如:0, 1, 1, 2, 3, 5, 8, 13, …

要求

  1. 输入:用户输入要生成的斐波那契数的数量 n。
  2. 生成:使用循环结构(如 for 或 while 循环)生成斐波那契数列。
  3. 输出:显示生成的斐波那契数列。
  4. 函数封装:将生成斐波那契数列的逻辑封装在一个独立的函数中。

示例输入与输出

示例 1:

1
2
3
请输入要生成的斐波那契数的数量: 10
斐波那契数列:
0 1 1 2 3 5 8 13 21 34

示例 2:

1
2
3
请输入要生成的斐波那契数的数量: 5
斐波那契数列:
0 1 1 2 3

提示

  • 斐波那契数列的定义

    :

    • 第 0 个斐波那契数是 0。
    • 第 1 个斐波那契数是 1。
    • 对于 n >= 2,第 n 个斐波那契数是第 n-1 个数和第 n-2 个数的和。
  • 实现方法

    :

    • 迭代法:使用循环结构依次计算斐波那契数。
    • 递归法(高级):使用递归函数实现,但效率较低,通常不推荐用于较大的 n。
  • 数据类型:根据 n 的范围选择合适的数据类型,unsigned long long 可以存储较大的斐波那契数。

参考答案(迭代法实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <vector>

// 生成斐波那契数列的函数(迭代法)
std::vector<unsigned long long> generateFibonacci(int n) {
std::vector<unsigned long long> fib;

if (n <= 0) {
// 返回空向量
return fib;
}

// 第一个斐波那契数
fib.push_back(0);

if (n == 1) {
return fib;
}

// 第二个斐波那契数
fib.push_back(1);

// 生成后续的斐波那契数
for (int i = 2; i < n; ++i) {
unsigned long long next = fib[i - 1] + fib[i - 2];
fib.push_back(next);
}

return fib;
}

// 打印斐波那契数列的函数
void printFibonacci(const std::vector<unsigned long long> &fib) {
for (size_t i = 0; i < fib.size(); ++i) {
std::cout << fib[i] << " ";
}
std::cout << std::endl;
}

int main() {
int n;

// 获取要生成的斐波那契数的数量
std::cout << "请输入要生成的斐波那契数的数量: ";
std::cin >> n;

if (n < 0) {
std::cerr << "错误:数量不能为负数。" << std::endl;
return 1;
}

// 生成斐波那契数列
std::vector<unsigned long long> fibonacci = generateFibonacci(n);

// 打印斐波那契数列
std::cout << "斐波那契数列:" << std::endl;
printFibonacci(fibonacci);

return 0;
}

代码解释

  1. **函数 generateFibonacci**:

    • 参数:接收一个整数 n,表示要生成的斐波那契数的数量。

    • 返回值:返回一个 std::vector<unsigned long long>,包含生成的斐波那契数列。

    • 逻辑

      :

      • 如果 n <= 0,返回一个空的向量。
      • 初始化斐波那契数列的前两个数:0 和 1。
      • 使用一个 for 循环,从第三个数开始,依次计算当前数为前两个数之和,并将其添加到向量中。
  2. **函数 printFibonacci**:

    • 参数:接收一个 const 引用的斐波那契数列向量。
    • 功能:遍历并打印斐波那契数列中的每个数,用空格分隔。
  3. main 函数:

    • 步骤

      :

      1. 输入数量:提示用户输入要生成的斐波那契数的数量 n。
      2. 输入验证:检查 n 是否为负数,若是则输出错误信息并终止程序。
      3. 生成数列:调用 generateFibonacci 函数生成斐波那契数列。
      4. 打印数列:调用 printFibonacci 函数显示生成的斐波那契数列。
  4. 数据类型选择:

    • 使用 unsigned long long 可以存储较大的斐波那契数,避免整数溢出。但需要注意,unsigned long long 的范围有限,对于非常大的 n,仍然会发生溢出。

运行示例

1
2
3
请输入要生成的斐波那契数的数量: 10
斐波那契数列:
0 1 1 2 3 5 8 13 21 34

递归实现参考代码(可选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <vector>
#include <stdexcept>

// 递归函数,计算第 n 个斐波那契数
unsigned long long fibonacciRecursive(int n) {
if (n < 0) {
throw std::invalid_argument("n 不能为负数。");
}
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}

// 生成斐波那契数列的函数(递归实现)
std::vector<unsigned long long> generateFibonacciRecursive(int n) {
std::vector<unsigned long long> fib;

for (int i = 0; i < n; ++i) {
fib.push_back(fibonacciRecursive(i));
}

return fib;
}

// 打印斐波那契数列的函数
void printFibonacci(const std::vector<unsigned long long> &fib) {
for (size_t i = 0; i < fib.size(); ++i) {
std::cout << fib[i] << " ";
}
std::cout << std::endl;
}

int main() {
int n;

// 获取要生成的斐波那契数的数量
std::cout << "请输入要生成的斐波那契数的数量: ";
std::cin >> n;

if (n < 0) {
std::cerr << "错误:数量不能为负数。" << std::endl;
return 1;
}

try {
// 生成斐波那契数列(递归实现)
std::vector<unsigned long long> fibonacci = generateFibonacciRecursive(n);

// 打印斐波那契数列
std::cout << "斐波那契数列:" << std::endl;
printFibonacci(fibonacci);
}
catch (const std::invalid_argument &e) {
std::cerr << "错误:" << e.what() << std::endl;
}

return 0;
}

递归实现说明

  • 优点

    :

    • 代码简洁,符合斐波那契数列的数学定义。
  • 缺点

    :

    • 时间复杂度为指数级 O(2^n),对于较大的 n 会非常低效。
    • 递归深度过大可能导致栈溢出。
  • 使用场景:适合学习和理解递归概念,但在实际应用中需谨慎使用。

动态规划方法

动态规划通过存储已计算的值来避免重复计算,提高了效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>

// 动态规划实现斐波那契数列
std::vector<int> fibonacci_dynamic(int n) {
std::vector<int> fib(n);
fib[0] = 0;
if (n > 1) {
fib[1] = 1;
}
for (int i = 2; i < n; ++i) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}

int main() {
int n;
std::cout << "请输入斐波那契数列的项数: ";
std::cin >> n;

std::vector<int> fib = fibonacci_dynamic(n);
std::cout << "斐波那契数列的前 " << n << " 项为: ";
for (int i = 0; i < n; ++i) {
std::cout << fib[i] << " ";
}
std::cout << std::endl;

return 0;
}

零基础C++(16) 常见运算符

Posted on 2024-10-26 | In 零基础C++

导言

运算符是编程语言中用于执行特定操作的符号或关键字。在C++中,运算符的使用广泛且多样,掌握运算符的使用对于编写高效、简洁的代码至关重要。本教案旨在全面介绍C++中的各种运算符,帮助学习者深入理解和灵活运用。

运算符概述

运算符(Operator) 是用来对变量进行操作的符号或函数。C++中的运算符可分为多种类型,每种运算符具有特定的功能和使用规则。运算符可以单目(仅操作一个操作数)、双目(操作两个操作数)、甚至三目(操作三个操作数)等。

运算符分类

C++中的运算符可以根据功能和使用方式分为以下几类:

1. 算术运算符

用于执行基本的数学计算。

运算符 描述 示例
+ 加法 a + b
- 减法 a - b
* 乘法 a * b
/ 除法 a / b
% 取模(求余数) a % b
++ 自增(前缀/后缀) ++a, a++
-- 自减(前缀/后缀) --a, a--

示例:

1
2
3
4
5
6
7
8
int a = 10, b = 3;
int sum = a + b; // 13
int diff = a - b; // 7
int prod = a * b; // 30
int div = a / b; // 3
int mod = a % b; // 1
a++; // a = 11
--b; // b = 2

2. 关系运算符

用于比较两个值之间的关系,返回布尔值(true 或 false)。

运算符 描述 示例
== 等于 a == b
!= 不等于 a != b
> 大于 a > b
< 小于 a < b
>= 大于或等于 a >= b
<= 小于或等于 a <= b

示例:

1
2
3
4
int a = 5, b = 10;
bool result1 = (a == b); // false
bool result2 = (a < b); // true
bool result3 = (a >= b); // false

3. 逻辑运算符

用于组合或反转布尔表达式,返回布尔值。

运算符 描述 示例
&& 逻辑与(AND) a && b
` `
! 逻辑非(NOT) !a

示例:

1
2
3
4
bool a = true, b = false;
bool result1 = a && b; // false
bool result2 = a || b; // true
bool result3 = !a; // false

4. 位运算符

用于按位操作整数类型的二进制位。

运算符 描述 示例
& 按位与 a & b
` ` 按位或
^ 按位异或(不等时为1) a ^ b
~ 按位取反 ~a
<< 左移 a << 2
>> 右移 a >> 2

示例:

1
2
3
4
5
6
7
8
int a = 5;  // 二进制:0101
int b = 3; // 二进制:0011
int andResult = a & b; // 1 (0001)
int orResult = a | b; // 7 (0111)
int xorResult = a ^ b; // 6 (0110)
int notResult = ~a; // -6 (补码)
int leftShift = a << 1; // 10 (1010)
int rightShift = a >> 1; // 2 (0010)

5. 赋值运算符

用于向变量赋值。

运算符 描述 示例
= 简单赋值 a = b
+= 加后赋值 a += b
-= 减后赋值 a -= b
*= 乘后赋值 a *= b
/= 除后赋值 a /= b
%= 取模后赋值 a %= b
&= 按位与后赋值 a &= b
` =` 按位或后赋值
^= 按位异或后赋值 a ^= b
<<= 左移后赋值 a <<= 2
>>= 右移后赋值 a >>= 2

示例:

1
2
3
4
5
int a = 5;
int b = 3;
a += b; // a = 8
a *= 2; // a = 16
a &= b; // a = 16 & 3 = 0

6. 复合赋值运算符

结合赋值与其他运算的运算符(如上表中所示的+=, -=, 等)。

示例:

1
2
int a = 10;
a += 5; // 等同于 a = a + 5; 结果 a = 15

7. 条件运算符

用于基于条件选择值。

运算符 描述 示例
?: 条件(三目)运算符 a ? b : c

示例:

1
2
int a = 10, b = 20, c;
c = (a > b) ? a : b; // c = 20

8. 递增和递减运算符

用于增加或减少变量的值,前缀和后缀形式。

运算符 描述 示例
++ 自增(前缀/后缀) ++a, a++
-- 自减(前缀/后缀) --a, a--

示例:

1
2
3
int a = 5;
int b = ++a; // a = 6, b = 6
int c = a--; // a = 5, c = 6

9. 指针运算符

用于操作指针。

运算符 描述 示例
* 间接访问(解引用) *ptr
& 取地址 &a
-> 成员访问(指向对象的指针) ptr->member
[] 数组下标访问 arr[2]

示例:

1
2
3
int a = 10;
int *ptr = &a;
int value = *ptr; // value = 10

10. 成员访问运算符

用于访问类或结构体的成员。

运算符 描述 示例
. 直接成员访问 object.member
-> 指向成员的指针访问 ptr->member
::* 指向成员的指针(成员指针操作符) Class::*ptr

示例:

1
2
3
4
5
6
7
8
9
struct Point {
int x;
int y;
};

Point p = {10, 20};
Point *ptr = &p;
int a = p.x; // 使用 . 运算符
int b = ptr->y; // 使用 -> 运算符

11. 其他运算符

运算符 描述 示例
sizeof 返回变量或类型所占字节数 sizeof(int)
?: 条件(三目)运算符 a ? b : c
, 逗号运算符 a = (b, c)
typeid 运行时类型信息运算符 typeid(a)
new 动态内存分配 int *ptr = new int;
delete 动态内存释放 delete ptr;

示例:

1
2
3
4
int a = 5;
int size = sizeof(a); // size = 4 (通常)
int b, c;
b = (a++, a + 2); // a = 6, b = 8

运算符优先级与结合性

运算符的优先级决定了在没有括号明确指定的情况下,哪一个运算符先被计算。结合性则决定了运算符在具有相同优先级时的计算顺序(从左到右或从右到左)。

优先级表

以下是C++运算符的优先级从高到低的简要概览:

优先级 运算符类别 运算符 结合性 备注
1 范围解析运算符 :: 左到右 用于访问命名空间或类的成员
2 后缀运算符 (), [], ., ->, ++(后置), --(后置) 左到右 包含函数调用、数组下标、成员访问
3 一元运算符 +, -, !, ~, ++(前置), --(前置), *(解引用), &(取地址), sizeof, typeid 右到左 适用于单个操作数的运算符
4 乘法运算符 *, /, % 左到右 乘法、除法和取模运算
5 加法运算符 +, - 左到右 加法和减法运算
6 移位运算符 <<, >> 左到右 位左移和位右移
7 关系运算符 <, <=, >, >= 左到右 比较运算符
8 相等运算符 ==, != 左到右 判断相等与不相等
9 位与运算符 & 左到右 按位与
10 位异或运算符 ^ 左到右 按位异或
11 位或运算符 ` ` 左到右
12 逻辑与运算符 && 左到右 逻辑与
13 逻辑或运算符 ` `
14 条件运算符 ?: 右到左 条件(三目)运算符
15 赋值运算符 =, +=, -=, *=, /=, %=, &=, ` =, ^=, <<=, >>=` 右到左
16 逗号运算符 , 左到右 逗号用于表达式中多个操作

表格说明

  • 优先级:数字越小,优先级越高。即优先级为1的运算符最先被计算。
  • 运算符类别:运算符的功能分类,帮助理解不同类型运算符的用途。
  • 运算符:具体的C++运算符符号。
  • 结合性:当表达式中出现多个相同优先级的运算符时,决定运算顺序的规则。左到右表示从左侧的操作数开始,右到左表示从右侧的操作数开始。
  • 备注:对运算符类别或特定运算符的简要说明。

运算符重载

运算符重载(Operator Overloading) 允许开发者为自定义类型(如类和结构体)定义或改变运算符的行为,使其表现得像内置类型一样。这提高了代码的可读性和可维护性。

运算符重载的规则

  1. 可重载运算符:几乎所有的运算符都可以被重载,但如 ::, ?:, sizeof 等运算符不能被重载。
  2. 至少一个操作数必须是用户定义类型:即至少有一个操作数是类、结构体或联合体类型。
  3. 运算符重载不改变运算符的优先级、结合性和操作数数量。

运算符重载的基本语法

运算符可以作为成员函数或友元函数进行重载。

成员函数重载示例:

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

Complex operator+(const Complex &c) {
Complex temp;
temp.real = real + c.real;
temp.imag = imag + c.imag;
return temp;
}
};

友元函数重载示例:

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

friend Complex operator+(const Complex &c1, const Complex &c2);
};

Complex operator+(const Complex &c1, const Complex &c2) {
Complex temp;
temp.real = c1.real + c2.real;
temp.imag = c1.imag + c2.imag;
return temp;
}

常见的重载运算符

  • 算术运算符:+, -, *, /, %
  • 关系运算符:==, !=, <, >, <=, >=
  • 逻辑运算符:&&, ||, !
  • 赋值运算符:=, +=, -=, *=, /=
  • 输入输出运算符:<<, >>
  • 索引运算符:[]
  • 函数调用运算符:()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

class Complex {
public:
double real, imag;

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

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

// 重载 << 运算符(作为友元函数)
friend ostream& operator<<(ostream &out, const Complex &c);
};

ostream& operator<<(ostream &out, const Complex &c) {
out << c.real << " + " << c.imag << "i";
return out;
}

int main() {
Complex c1(1.2, 3.4);
Complex c2(5.6, 7.8);
Complex c3 = c1 + c2;
cout << "c1 + c2 = " << c3 << endl; // 输出: c1 + c2 = 6.8 + 11.2i
return 0;
}

练习题

1 交换两个数

题目: 使用位运算符,交换两个整数变量的值而不使用第三个变量。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
int main() {
int x = 15;
int y = 27;

std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;

// 交换操作
x = x ^ y;
y = x ^ y;
x = x ^ y;

std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

return 0;
}

预期输出:

1
2
Before swap: x = 15, y = 27
After swap: x = 27, y = 15

解析: 通过异或运算 ^ 完成变量值的交换,无需使用临时变量。

2 函数修改外部变量

题目: 编写一个函数,接受一个整数指针,使用解引用运算符修改其值为原值的平方。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
void square(int* ptr) {
*ptr = (*ptr) * (*ptr);
}

int main() {
int num = 5;
std::cout << "Before: " << num << std::endl;
square(&num);
std::cout << "After: " << num << std::endl;
return 0;
}

预期输出:

1
2
Before: 5
After: 25

解析: 通过指针访问并修改原变量的值。

3 计算范围内所有元素的和

题目: 编写一个函数,接受 std::vector<int> 的迭代器范围,计算并返回范围内所有元素的和。

函数示例:

须实现如下函数,返回范围内元素求和的结果

1
int sumRange(std::vector<int>::iterator start, std::vector<int>::iterator end);

答案:

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

int sumRange(std::vector<int>::iterator start, std::vector<int>::iterator end) {
int sum = 0;
while (start != end) {
sum += *start;
++start;
}
return sum;
}

int main() {
std::vector<int> numbers = {2, 4, 6, 8, 10};
int total = sumRange(numbers.begin(), numbers.end());
std::cout << "Sum: " << total << std::endl;
return 0;
}

预期输出:

1
Sum: 30

解析: 函数通过迭代器遍历范围,累加元素值。

零基础C++(15) 多维数组

Posted on 2024-10-22 | In 零基础C++

多维数组

更多资料可查阅官方博客,官方博客地址:https://llfc.club/

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组大有益处。

当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:

1
2
// 大小为3的数组,每个元素是大小为4的数组
int ia[3][4];

按照由内而外的顺序阅读此类定义有助于更好地理解其真实含义。

在第一条语句中,我们定义的名字是ia,显然ia是一个含有3个元素的数组。

接着观察右边发现,ia的元素也有自己的维度,所以ia的元素本身又都是含有4个元素的数组。

再观察左边知道,真正存储的元素是整数。因此最后可以明确第一条语句的含义:它定义了一个大小为3的数组,该数组的每个元素都是含有4个整数的数组。

上面的代码可以理解为下面的形式

https://cdn.llfc.club/1729729218328.jpg-llfc

也可以初始化为

1
2
// 这些数组的元素是含有30个整数的数组
int arr[10][20][30] = {0}

使用同样的方式理解arr的定义。

首先arr是一个大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素又都是含有30个整数的数组。

实际上,定义数组时对下标运算符的数量并没有限制,因此只要愿意就可以定义这样一个数组:它的元素还是数组,下一级数组的元素还是数组,再下一级数组的元素还是数组,以此类推。对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

多维数组的初始化

允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。下面的初始化形式中,多维数组的每一行分别用花括号括了起来:

1
2
3
4
5
6
7
8
9
//三个元素,每个元素是大小为4的数组
int ia[3][4] ={
//第一行的初始值
{0,1,2,3},
//第二行初始值
{4,5,6,7},
//第三行初始值
{8,9,10,11}
};

其中内层嵌套着的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的功能和上面这段代码完全一样:

1
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化列表之内。如果仅仅想初始化每一行的第一个元素,通过如下的语句即可:

1
2
//初始化每一行的首元素
int ia2[3][4] = {{0},{4},{8}};

其他未列出的元素执行默认值初始化,这个过程和一维数组一样。在这种情况下如果再省略掉内层的花括号,结果就大不一样了。

1
2
//值初始化第一i行
int ix[3][4] = {0,3,5,9};

含义发生了变化,它初始化的是第一行的4个元素,其他元素被初始化为0。

多维数组的下标

引用可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。

如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;

反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

1
2
3
4
5
6
7
8
int ia[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
int arr[1][1][1] = {{{1}}};
// 用arr的首元素为ia的最后一个元素赋值
ia[2][3] = arr[0][0][0];
//row是一个4维数组的引用,将row绑定到ia的第二个元素(4维数组)上
int (&row)[4] = ia[1];

使用for循环

我们可以使用for循环构建数组

1
2
3
4
5
6
7
8
9
10
constexpr size_t rowCnt = 3, colCnt=4;
//12 个未初始化的元素
int ia[rowCnt][colCnt];
//对于每一行
for(size_t i = 0; i != rowCnt; ++i){
//对于行内的每一列
for( size_t j = 0; j != colCnt; ++j){
ia[i][j] = i*colCnt + j;
}
}

C++11风格处理多维数组

由于C++11新标准增加了范围for语句,所以前一个程序可以简化为

1
2
3
4
5
6
7
8
9
10
constexpr size_t rowCnt = 3, colCnt=4;
//12 个未初始化的元素
int ia[rowCnt][colCnt];
size_t cnt = 0;
for(auto &row: ia){
for(auto & col : row){
col = cnt;
++cnt;
}
}

输出每一个元素

1
2
3
4
5
6
for(const auto & row: ia){
for(auto col : row){
std::cout << col << " ";
}
std::cout << std::endl;
}

输出

1
2
3
0 1 2 3
4 5 6 7
8 9 10 11

指针和多维数组

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

新手雷区

定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。

因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

1
2
3
4
5
6
//大小为3的数组,每个元素是含有4个整数的数组
int ia[3][4];
//p指向含有4个整数的数组
int(*p)[4] = ia;
//将p修改为指向ia数组的尾部
p = &ia[2];

随着C++11新标准的提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ia数组
int ia[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
//输出ia中每个元素的值,每个内存数组各占一行
//p指向含有4个整数的数组
for(auto p = ia; p != ia + 3; ++p){
//q指向4个整数的数组的首元素
for(auto q = *p; q != *p + 4; ++q){
std::cout << *q << ' ';
}
std::cout << std::endl;
}

使用C++11提供的std::begin也能实现类似的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ia数组
int ia[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};

// p指向ia的第一个数组
for(auto p = std::begin(ia); p != std::end(ia); ++p){
// q指向内存数组的首元素
for( auto q = std::begin(*p); q != std::end(*p); ++q){
// 输出q所指的整数值
std::cout << *q << ' ';
}
std::cout << std::endl;
}

类型别名简化多维数组指针

可以使用using 进行类型别名的声明,或者使用typedef声明类型的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ia数组
int ia[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
// 新标准下类型别名的声明
using int_array = int[4];
// 使用typedef 声明类型的别名
typedef int int_array_t[4];

for(int_array * p = ia; p != ia + 3; ++p){
for(int *q = *p ; q != *p+4; ++q){
std::cout << *q << " ";
}
std::cout << std::endl;
}

练习题1:矩阵加法

题目描述

编写一个C++程序,输入两个2x3的矩阵,计算它们的和,并输出结果矩阵。

示例代码框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

int main() {
const int ROW = 2;
const int COL = 3;
int matrix1[ROW][COL];
int matrix2[ROW][COL];
int sum[ROW][COL];

// 输入第一个矩阵
std::cout << "请输入第一个2x3矩阵的元素(共6个整数):" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
// 在此输入元素
}
}

// 输入第二个矩阵
std::cout << "请输入第二个2x3矩阵的元素(共6个整数):" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
// 在此输入元素
}
}

// 计算两个矩阵的和
// 在此实现加法逻辑

// 输出结果矩阵
std::cout << "两个矩阵的和为:" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
// 在此输出sum[i][j]
}
std::cout << std::endl;
}

return 0;
}

预期输出(示例)

1
2
3
4
5
6
7
请输入第一个2x3矩阵的元素(共6个整数):
1 2 3 4 5 6
请输入第二个2x3矩阵的元素(共6个整数):
6 5 4 3 2 1
两个矩阵的和为:
7 7 7
7 7 7

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

int main() {
const int ROW = 2;
const int COL = 3;
int matrix1[ROW][COL];
int matrix2[ROW][COL];
int sum[ROW][COL];

// 输入第一个矩阵
std::cout << "请输入第一个2x3矩阵的元素(共6个整数):" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
std::cin >> matrix1[i][j];
}
}

// 输入第二个矩阵
std::cout << "请输入第二个2x3矩阵的元素(共6个整数):" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
std::cin >> matrix2[i][j];
}
}

// 计算两个矩阵的和
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
sum[i][j] = matrix1[i][j] + matrix2[i][j];
}
}

// 输出结果矩阵
std::cout << "两个矩阵的和为:" << std::endl;
for(int i = 0; i < ROW; ++i) {
for(int j = 0; j < COL; ++j) {
std::cout << sum[i][j] << " ";
}
std::cout << std::endl;
}

return 0;
}

练习题2:矩阵转置

题目描述

编写一个C++程序,输入一个3x3的矩阵,计算其转置矩阵,并输出结果。

示例代码框架

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>

int main() {
const int SIZE = 3;
int matrix[SIZE][SIZE];
int transpose[SIZE][SIZE];

// 输入原始矩阵
std::cout << "请输入一个3x3矩阵的元素(共9个整数):" << std::endl;
for(int i = 0; i < SIZE; ++i) {
for(int j = 0; j < SIZE; ++j) {
// 在此输入matrix[i][j]
}
}

// 计算转置矩阵
// 在此实现转置逻辑

// 输出转置后的矩阵
std::cout << "矩阵的转置为:" << std::endl;
for(int i = 0; i < SIZE; ++i) {
for(int j = 0; j < SIZE; ++j) {
// 在此输出transpose[i][j]
}
std::cout << std::endl;
}

return 0;
}

预期输出(示例)

1
2
3
4
5
6
请输入一个3x3矩阵的元素(共9个整数):
1 2 3 4 5 6 7 8 9
矩阵的转置为:
1 4 7
2 5 8
3 6 9

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

int main() {
const int SIZE = 3;
int matrix[SIZE][SIZE];
int transpose[SIZE][SIZE];

// 输入原始矩阵
std::cout << "请输入一个3x3矩阵的元素(共9个整数):" << std::endl;
for(int i = 0; i < SIZE; ++i) {
for(int j = 0; j < SIZE; ++j) {
std::cin >> matrix[i][j];
}
}

// 计算转置矩阵
for(int i = 0; i < SIZE; ++i) {
for(int j = 0; j < SIZE; ++j) {
transpose[j][i] = matrix[i][j];
}
}

// 输出转置后的矩阵
std::cout << "矩阵的转置为:" << std::endl;
for(int i = 0; i < SIZE; ++i) {
for(int j = 0; j < SIZE; ++j) {
std::cout << transpose[i][j] << " ";
}
std::cout << std::endl;
}

return 0;
}

赞赏

感谢支持

https://cdn.llfc.club/dashang.jpg

零基础C++(14) 数组知识

Posted on 2024-10-20 | In 零基础C++

数组概念

数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。

与vector对比

相同点

与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。

不同点

与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。

友情提示

如果不清楚元素的确切个数,请使用vector。

定义和初始化内置数组

数组是一种复合类型。数组的声明形如

1
类型 a[d];

其中a是数组的名字,d是数组的维度。

维度说明了数组中元素的个数,因此必须大于0。

数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式

关于常量表达式我们可以复习一下

1
2
3
4
//不是常量表达式
unsigned int cnt = 42;
//常量表达式, 用constexpr修饰
constexpr unsigned sz = 42;

定义数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
//不是常量表达式
unsigned cnt = 42;
//常量表达式, 用constexpr修饰
constexpr unsigned sz = 42;
//包含10个整数的数组
int arr[10];
//含有42个整数指针的数组
int *parr[sz];
//定义字符串数组,错误!cnt不是常量表达式,但是部分编译器可通过
std::string bad[cnt];

return 0;
}

和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

注意

定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。

显式初始化数组元素

可以对数组的元素进行列表初始化,此时允许忽略数组的维度。

如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;

相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值:

1
2
3
4
5
6
7
8
9
10
11
const unsigned sz = 3;
// 含有3个元素的数组,元素值分别是0,1,2
int ial[sz] = {0,1,2};
// 维度是3的数组
int a2[] = {0,1,2};
//等价于a3[] = {0,1,2,0,0}
int a3[5] = {0,1,2};
//等价于a4[] = {"hi","bye",""}
std::string a4[3] = {"hi","bye"};
//错误,初始值过多
//int a5[2] = {0,1,2};

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:

1
2
3
4
//含有三个整数的数组
int a[] = {0,1,2};
// 错误,不允许使用一个数组初始化另一个数组
// int a2[] = a;

友情提示

一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。

理解复杂的数组声明

和vector一样,数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。

在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了:

1
2
3
4
5
6
7
8
9
//ptrs是含有10个整数指针的数组
int *ptrs[10];
//错误, 不存在引用的数组
//int& refs[10] = /*?*/;
//Parray指向一个含有10个整数的数组
int arr[10] ={0,1,2,3,4,5,6,7,8,9};
int (*Parray)[10] = &arr;
//arrRef 引用一个含有10个整数的数组
int (&arrRef)[10] = arr;

要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。

访问数组元素

与标准库类型vector和string一样,数组的元素也能使用范围for语句或下标运算符来访问。数组的索引从0开始,以一个包含10个元素的数组为例,它的索引从0到9,而非从1到10。

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。

1
2
3
4
5
int arr[10] ={0,1,2,3,4,5,6,7,8,9};
for(size_t i = 0; i < sizeof(arr)/sizeof(int); ++i){
std::cout << arr[i] << " ";
}
std::cout << std::endl;

数组的大小可以用sizeof(arr)获取,要进一步计算获取其中的元素个数,我们可以使用sizeof(arr)/sizeof(int)

防止越界

数组不具备越界检测,所以在使用下标访问数组元素的时候,一定要注意防止越界,不要超过或等于数组元素个数

指针和数组

在C++语言中,指针和数组有非常紧密的联系。就如即将介绍的,使用数组的时候编译器一般会把它转换成指针。

通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。

数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

1
2
3
4
5
6
7
int arr[10] ={0,1,2,3,4,5,6,7,8,9};
// 第一个元素地址
std::cout << "first element: address is " << &arr[0] << std::endl;
// 数组首地址
std::cout << "arr address is " << arr << std::endl;
// 数组首地址
std::cout << "arr address is " << &arr << std::endl;

数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:

1
2
3
//等价于 int * first_elem_addr = &arr[0];
int* first_elem_addr = arr;
std::cout << "first element address is " << first_elem_addr << std::endl;

在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组

1
2
//ia2是一个int类型的指针,指向ia的第一个元素
auto ia2(arr);

当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由10个整数构成的数组:

1
2
3
4
//ia3是一个含有10个整数的数组
decltype(arr) ia3 = {0,1,2,3,4,5,6,7,8,9};
//错误,不能用整数指针给数组赋值
//ia3 = ia2;

指针也是迭代器

介绍的内容相比,指向数组元素的指针拥有更多功能。

vector和string的迭代器。支持的运算,数组的指针全都支持。

例如,允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上:

1
2
3
4
5
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
//p指向arr的第一个元素
int *p = arr;
//p指向arr[1]
++p;

奇技淫巧

就像使用迭代器遍历vector对象中的元素一样,使用指针也能遍历数组中的元素。当然,这样做的前提是先得获取到指向数组第一个元素的指针和指向数组尾元素的下一位置的指针。

1
2
3
4
5
6
7
8
9
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
// e指向arr[10],也就是最后一个元素的下一个位置
int *e = arr+10;

for(int* b = arr; b != e; ++b){
std::cout << *b << " ";
}

std::cout << std::endl;

C++11的改进

为了方便遍历数组,C++11提供了获取最后元素的下一个位置的指针,以及指向首元素的指针

1
2
3
4
5
6
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int * beg = std::begin(ia);
int * end = std::end(ia);
for(auto it = beg; it != end; ++it){
std::cout << *it << " ";
}

指针运算

指向数组元素的指针可以执行的运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。

给(从)一个指针加上(减去)某整数值,结果仍是指针。新指针指向的元素与原来的指针相比前进了(后退了)该整数值个位置:

1
2
3
4
5
6
constexpr size_t sz = 5;
int arr[sz] = {0,1,2,3,4};
//等价于int *ip = &arr[0];
int *ip = arr;
//ip2
int * ip2 = ip + 4;

和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素:

1
2
3
//计算数组元素个数
auto n = std::end(arr) - std::begin(arr);
std::cout << "n is " << n << std::endl;

解引用和指针运算的交互

指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针:

1
2
3
int ia[] = {0,2,4,6,8};
int last = *(ia+4);
std::cout << "last is " << last << std::endl;

表达式*(ia+4)计算ia前进4个元素后的新地址,解引用该结果指针的效果等价于表达式ia[4]。

如果写成下面的形式:

1
2
3
int ia[] = {0,2,4,6,8};
//等价于ia[0] + 4
int value = *ia + 4;

下标和指针的关系

对数组执行下标运算其实是对指向数组元素的指针解引用

1
2
3
int ia[] = {0,2,4,6,8};
//等价于ia[1]
int value = *(ia+1);

C风格字符串

尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。

字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(’\0’)。一般利用指针来操作这些字符串。

1
char* msg = "hello world!";

C标准库函数

这些函数可用于操作C风格字符串,它们定义在cstring头文件中,cstring是C语言头文件string.h的C++版本。

函数示例 功能解释
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1,p2) 比较p1和p2的是否相等,如果相等返回0,如果p1>p2返回一个正值,如果p1<p2返回一个负值
strcat(p1,p2) 将p2附加到p1之后,返回p1
strcpy(p1,p2) 将p2拷贝给p1,返回p1

新手雷区

传入此类函数的指针必须指向以空字符作为结束的数组:

1
2
3
4
char ca[] = {'C','P','P'};
//有风险,因为ca没有以\0结束,所以strlen可能访问越界
int len = strlen(ca);
std::cout << "len is " << len << std::endl;

此例中,ca虽然也是一个字符数组但它不是以空字符作为结束的,因此上述程序将产生未定义的结果。strlen函数将有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。

比较字符串

比较两个C风格字符串的方法和之前学习过的比较标准库string对象的方法大相径庭。比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符:

1
2
3
4
5
6
7
std::string s1 = "A string example";
std::string s2 = "A different string example";
if(s1 < s2){
std::cout << "s1 is less than s2" << std::endl;
}else{
std::cout << "s1 is not less than s2" << std::endl;
}

如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

1
2
3
4
5
6
const char ca1[] = "A string example";
const char ca2[] = "A different string example";
//未定义的,视图比较两个无关地址
if(ca1 < ca2){

}

要想比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了。如果两个字符串相等,strcmp返回0;如果前面的字符串较大,返回正值;如果后面的字符串较大,返回负值:

1
2
3
4
5
6
//和两个string比较大小功能一样
if(strcmp(ca1, ca2) < 0){
std::cout << "ca1 is less than ca2" << std::endl;
}else{
std::cout << "ca1 is not less than ca2" << std::endl;
}

字符串拼接

字符串拼接可采用strcpy

1
2
3
4
5
6
char dest[20] = "Hello, "; // 确保有足够的空间
const char *src = "World!";

// 使用strcpy
strcpy(dest + strlen(dest), src); // 从dest的末尾开始复制src
std::cout << "After strcpy: " << dest << std::endl;

strcat连接

1
2
3
4
5
// 另一个例子,直接使用strcat
const char *src = "World!";
char anotherDest[40] = "Hello, ";
strcat(anotherDest, src);
std::cout << "After strcat: " << anotherDest << std::endl;

与旧代码衔接

很多C++程序在标准库出现之前就已经写成了,它们肯定没用到string和vector类型。而且,有一些C++程序实际上是与C语言或其他语言的接口程序,当然也无法使用C++标准库。因此,现代的C++程序不得不与那些充满了数组和/或C风格字符串的代码衔接,为了使这一工作简单易行,C++专门提供了一组功能。

混用string对象和C风格字符串

1
2
3
std::string s("Hello World");
//注意返回const char *
const char *str = s.c_str();

顾名思义,c_str函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。

我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。

使用数组初始化vector对象

介绍过不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。

相反的,允许使用数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:

1
2
3
4
5
int int_arr[] = {0,1,2,3,4,5};
std::vector<int> ivec(std::begin(int_arr), std::end(int_arr));
for(auto e : ivec){
std::cout << e << " ";
}

练习题1:

题目描述

编写一个函数 my_strcpy,其功能与标准库函数 strcpy 类似,用于将源字符串复制到目标字符串中。

函数原型

1
char* my_strcpy(char* dest, const char* src);

要求

  • 禁止使用标准库中的字符串操作函数(如 strcpy、strlen 等)。
  • 函数应能够正确处理所有合法的C风格字符串,包括空字符串。
  • 确保目标字符串有足够的内存来存放源字符串。
  • 函数应返回目标字符串的指针。

示例代码框架

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

// 自定义的字符串复制函数
char* my_strcpy(char* dest, const char* src) {
// 在此实现函数逻辑
}

int main() {
const char* source = "Hello, World!";
char destination[50]; // 确保目标有足够的空间

my_strcpy(destination, source);

std::cout << "Source: " << source << std::endl;
std::cout << "Destination: " << destination << std::endl;

return 0;
}

预期输出

1
2
Source: Hello, World!
Destination: Hello, World!

提示

  • 遍历源字符串,逐个字符复制到目标字符串,直到遇到字符串结束符'\0'。
  • 不要忘记在目标字符串末尾添加结束符'\0'。

答案实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 自定义的字符串复制函数
char* my_strcpy(char* dest, const char* src) {
// 使用一个指针遍历源字符串
char* original_dest = dest; // 保存目标字符串的起始地址

while (*src != '\0') { // 当源字符不是结束符
*dest = *src; // 复制字符
dest++; // 移动目标指针
src++; // 移动源指针
}
*dest = '\0'; // 在目标字符串末尾添加结束符

return original_dest; // 返回目标字符串的起始地址
}

int main() {
const char* source = "Hello, World!";
char destination[50]; // 确保目标有足够的空间

my_strcpy(destination, source);

std::cout << "Source: " << source << std::endl;
std::cout << "Destination: " << destination << std::endl;

return 0;
}

代码解释

  1. 函数原型:

    1
    char* my_strcpy(char* dest, const char* src);
    • 参数

      :

      • dest: 目标字符串的指针,指向预先分配好的足够空间的字符数组。
      • src: 源字符串的指针,指向需要复制的字符串。
    • 返回值: 返回目标字符串的指针,以便于链式调用。

  2. 实现细节:

    • 使用一个临时指针 original_dest 保存 dest 的起始地址,以便在函数结束时返回。
    • 使用一个 while 循环遍历源字符串,逐个字符复制到目标字符串。
    • 当源字符串的当前字符为 '\0' 时,结束复制,并在目标字符串末尾添加 '\0' 确保字符串终止。
    • 返回 original_dest 指针。
  3. 主函数:

    • 定义一个源字符串 source。
    • 定义一个足够大的目标字符数组 destination。
    • 调用 my_strcpy 函数进行复制。
    • 输出源字符串和目标字符串以验证复制的正确性。

预期输出

1
2
Source: Hello, World!
Destination: Hello, World!

注意事项

  • 内存分配: 确保 dest 指向的内存区域足够大,以容纳源字符串和结束符 '\0'。
  • 安全性: 本实现没有进行边界检查。在实际应用中,建议使用更安全的方法,如strncpy,以防止缓冲区溢出。

练习题2:

题目描述

编写一个函数 my_strcat,其功能与标准库函数 strcat 类似,用于将源字符串追加到目标字符串的末尾。

函数原型

1
char* my_strcat(char* dest, const char* src);

要求

  • 禁止使用标准库中的字符串操作函数(如 strcat、strlen 等)。
  • 函数应能够正确处理所有合法的C风格字符串,包括空字符串。
  • 确保目标字符串有足够的内存来存放追加后的字符串。
  • 函数应返回目标字符串的指针。

示例代码框架

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

// 自定义的字符串连接函数
char* my_strcat(char* dest, const char* src) {
// 在此实现函数逻辑
}

int main() {
char destination[100] = "Hello, "; // 初始内容
const char* source = "World!";

my_strcat(destination, source);

std::cout << "After concatenation: " << destination << std::endl;

return 0;
}

预期输出

1
After concatenation: Hello, World!

提示

  • 首先找到目标字符串中的结束符'\0',然后从那里开始复制源字符串的内容。
  • 确保在追加完成后,目标字符串依然以'\0'结尾。

答案实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

// 自定义的字符串连接函数
char* my_strcat(char* dest, const char* src) {
char* original_dest = dest; // 保存目标字符串的起始地址

// 找到目标字符串的结束位置
while (*dest != '\0') {
dest++;
}

// 从源字符串复制字符到目标字符串末尾
while (*src != '\0') {
*dest = *src;
dest++;
src++;
}

*dest = '\0'; // 在连接后的字符串末尾添加结束符

return original_dest; // 返回目标字符串的起始地址
}

int main() {
char destination[100] = "Hello, "; // 初始内容
const char* source = "World!";

my_strcat(destination, source);

std::cout << "After concatenation: " << destination << std::endl;

return 0;
}

代码解释

  1. 函数原型:

    1
    char* my_strcat(char* dest, const char* src);
    • 参数

      :

      • dest: 目标字符串的指针,指向预先分配好的足够空间的字符数组,并且该数组包含一个以 '\0' 结尾的合法C风格字符串。
      • src: 源字符串的指针,指向需要追加的字符串。
    • 返回值: 返回目标字符串的指针,以便于链式调用。

  2. 实现细节:

    • 使用一个临时指针 original_dest 保存 dest 的起始地址,以便在函数结束时返回。
    • 首先,使用一个 while 循环找到目标字符串 dest 的结束符 '\0',使指针 dest 指向字符串的末尾。
    • 然后,使用另一个 while 循环将源字符串 src 的字符一个个复制到 dest 的末尾。
    • 在复制结束后,添加 '\0' 结束符。
    • 返回 original_dest 指针。
  3. 主函数:

    • 初始化目标字符数组 destination 为 "Hello, ",并确保其有足够的空间容纳追加的内容。
    • 定义一个源字符串 source 为 "World!"。
    • 调用 my_strcat 函数将 source 追加到 destination。
    • 输出连接后的字符串以验证结果。

预期输出

1
After concatenation: Hello, World!

注意事项

  • 内存分配: 确保 dest 指向的内存区域足够大,以容纳原始字符串、追加的字符串以及结束符 '\0'。
  • 字符串终止: 在追加完成后,务必在目标字符串末尾添加 '\0',以确保字符串正确终止。
  • 安全性: 本实现没有进行边界检查。在实际应用中,建议使用更安全的方法,如strncat,以防止缓冲区溢出。

赞赏

感谢支持

https://cdn.llfc.club/dashang.jpg

零基础C++(13) 迭代器用法

Posted on 2024-10-12 | In 零基础C++

迭代器简介

迭代器(Iterator)是C++标准模板库(STL)中的一个重要概念,它提供了一种方法,按顺序访问容器(如vector, list, map等)中的元素,而无需暴露容器的内部表示。迭代器就像是一个指针,但它比指针更加安全,因为它只能访问容器内的元素,并且它的类型与容器紧密相关。

使用迭代器

和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。如有下述语句:

1
auto b = v.begin(), e = v.end(); //b和e的类型相同

end成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。

这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。

特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。

一般来说,我们不清楚(不在意)迭代器准确的类型到底是什么。在上面的例子中,使用auto关键字定义变量b和e,这两个变量的类型也就是begin和end的返回值类型,之后将对相关内容做更详细的介绍。

迭代器运算

符表3.6列举了迭代器支持的一些运算。使用==和!=来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。

https://cdn.llfc.club/1728711512535.jpg

和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。

试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。举个例子,利用下标运算符把string对象的第一个字母改为了大写形式,下面利用迭代器实现同样的功能:

比较运算

1
2
3
4
5
6
7
std::string s("some string");
//确保s非空
if(s.begin() != s.end()){
//第一个字母改为大写
auto it = s.begin();
*it = toupper(*it);
}

本例和原来的程序一样,首先检查s是否为空,显然通过检查begin和end返回的结果是否一致就能做到这一点。如果返回的结果一样,说明s为空;

如果返回的结果不一样,说明s不为空,此时s中至少包含一个字符。

我们在if内部,声明了一个迭代器变量it并把begin返回的结果赋给它,这样就得到了指示s中第一个字符的迭代器,接下来通过解引用运算符将第一个字符更改为大写形式。

和原来的程序一样,输出结果将是:

1
Some string

自增运算

将迭代器从一个元素移动到另外一个元素迭代器使用递增(++)运算符。

来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增和整数的递增类似,整数的递增是在整数值上“加1”,迭代器的递增则是将迭代器“向前移动一个位置”。

注意

因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。

把字符串中的第一个单词改为大写

1
2
3
4
5
6
std::string s2 = "another string";
for(auto it = s2.begin(); it != s2.end() &&
!isspace(*it); ++it) {
*it = toupper(*it);
}
std::cout << s2 << std::endl;

输出

1
ANOTHER string

循环首先用s.begin的返回值来初始化it,意味着it指示的是s中的第一个字符(如果有的话)。

条件部分检查是否已到达s的尾部,如果尚未到达,则将it解引用的结果传入isspace函数检查是否遇到了空白。

每次迭代的最后,执行++it令迭代器前移一个位置以访问s的下一个字符。

循环体内部和上一个程序if语句内的最后一句话一样,先解引用it,然后将结果传入toupper函数得到该字母对应的大写形式,再把这个大写字母重新赋值给it所指示的字符。

关键概念:泛型编程

原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪,

C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。

之前已经说过,只有string和vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

迭代器类型

就像不知道string和vector的size_type成员到底是什么类型一样,一般来说我们无须知道迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:

1
2
3
4
5
6
7
8
// 迭代器it, it能读写vector<int>的元素
std::vector<int>::iterator it;
// it2能读写string对象的字符
std::string::iterator it2;
// it3只能读元素,不能写元素
std::vector<int>::const_iterator it3;
// it4只能读字符,不能写字符
std::string::const_iterator it4;

const_iterator和指向常量的指针差不多,能读取但不能修改它所指的元素值。相反,iterator的对象可读可写。

如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator。

1
2
3
4
5
6
7
8
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用 const_iterator 遍历
std::vector<int>::const_iterator it;
for (it = numbers.cbegin(); it != numbers.cend(); ++it) {
std::cout << *it << " "; // 读取元素值
}
std::cout << std::endl;

术语:迭代器和迭代器类型

迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。

重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。

每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。

begin和end运算符

begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator:

1
2
3
4
5
6
std::vector<int> v;
const std::vector<int> cv;
//it1是 vector<int>的迭代器,
auto it1 = v.begin();
//it2是const vector<int>的迭代器
auto it2 = cv.begin();

c++11

如果一个容器非常量,我们也可以通过分别是cbegin和cend:获取对应的常量迭代器

1
2
//it3的类型是vector<int>::const_iterator
auto it3 = v.cbegin();

结合解引用和成员访问操作

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了,其代码如下所示:

1
(*it).empty()

(*it).empty()中的圆括号必不可少,该表达式的含义是先对it解引用,然后解引用的结果再执行点运算符。

如果不加圆括号,点运算符将由it来执行将报错

完整案例

1
2
3
4
5
6
7
std::vector<std::string> vs = {"hello", "world"};
for(auto it = vs.begin(); it != vs.end(); ++it){
//(*it)解引用获取string对象,再次调用empty()方法判断为空
if((*it).empty()){
std::cout << "empty string" << std::endl;
}
}

为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。

例如,假设用一个名为text的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出text中第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止:

1
2
3
4
5
6
7
8
9
//依次输出text的每一行直到遇到第一个空行为止
std::vector<std::string> text = {
"hello",
"",
"world",
};
for(auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it) {
std::cout << *it << std::endl;
}

我们首先初始化it令其指向text的第一个元素,循环重复执行直至处理完了text的所有元素或者发现某个元素为空。

每次迭代时只要发现还有元素并且尚未遇到空元素,就输出当前正在处理的元素。

值得注意的是,因为循环从头到尾只是读取text的元素而未向其中写值,所以使用了cbegin和cend来控制整个迭代过程。

迭代器失效

曾经介绍过,虽然vector对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。

1
2
3
4
5
//注意下面逻辑错误,在for循环中push元素导致死循环
std::vector<int> numbers = {1, 2, 3, 4, 5};
for(auto i = 0; i < numbers.size(); ++i) {
numbers.push_back(i);
}

也不要在循环中执行push操作

1
2
3
4
//注意下面逻辑错误,在for循环中push元素导致迭代器失效,也会导致死循环
for(auto it = numbers.begin(); it != numbers.end(); ++it) {
numbers.push_back(1);
}

同样我们执行删除操作也要注意,我们可以通过vector的erase操作删除迭代器指向的元素

1
2
//删除第一个元素
numbers.erase(numbers.begin() );

erase会返回删除元素的下一个元素的迭代器

面试题

vector容器存储了一系列数字,在循环中遍历每一个元素,并且删除其中的奇数,要求循环结束,vector元素为偶数,要求时间复杂度o(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::vector<int> numbers = {1, 2, 3, 4, 5};
//循环遍历,并删除其中奇数
for(auto it = numbers.begin(); it != numbers.end(); ) {
// 删除奇数
if(*it % 2 != 0){
it = numbers.erase(it);
continue;
}
++it;
}

for(auto num : numbers) {
std::cout << num << " ";
}

std::cout << std::endl;

迭代器运算

迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。

类似的,也能用==和!=对任意标准库类型的两个有效迭代器,进行比较。

string和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作迭代器运算(iterator arithmetic)。

https://cdn.llfc.club/1728788292262.jpg

迭代器的算术运算

可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。

执行这样的操作时,结果迭代器或者指示原vector对象(或string对象)内的一个元素,或者指示原vector对象(或string对象)尾元素的下一位置。

举个例子,下面的代码得到一个迭代器,它指向某vector对象中间位置的元素:

1
2
3
4
5
6
7
8
9
std::vector<int> numbers = {1, 2, 3, 4, 5};
//中间位置的迭代器
auto mid = numbers.begin() + numbers.size()/2;
//判断迭代器是否有效
if(mid != numbers.end()){
std::cout << *mid << std::endl;
}else{
std::cout << "mid is end" << std::endl;
}

mid指向了中间的元素3

使用迭代器运算

使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。

二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;

如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜素;

假如该元素大于要找的元素,则在序列的前半部分继续搜索。

在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索。

下面的程序使用迭代器完成了二分搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std::vector<int> numbers = {1, 2, 3, 4, 5};
//二分查找4所在的迭代器为止
auto beg = numbers.begin(), end = numbers.end();
auto mid = beg + (end - beg) / 2;
//二分查找
while(mid != end && *mid != 4){
//4在mid的右边
if(*mid < 4){
beg = mid + 1;
}else{ //4在mid的左边
end = mid;
}
mid = beg + (end - beg) / 2;

}

if(mid != end){
std::cout << "4 is found" << std::endl;
}else{
std::cout << "4 is not found" << std::endl;
}

程序的一开始定义了三个迭代器:beg指向搜索范围内的第一个元素、end指向尾元素的下一位置、mid指向中间的那个元素。

初始状态下,搜索范围是名为numbers的vector<int>的全部范围。

循环部分先检查搜索范围是否为空,如果mid和end的当前值相等,说明已经找遍了所有元素。

此时条件不满足,循环终止。当搜索范围不为空时,可知mid指向了某个元素,检查该元素是否就是我们所要搜索的,如果是,也终止循环。

当进入到循环体内部后,程序通过某种规则移动beg或者end来缩小搜索的范围。

如果mid所指的元素比要找的元素4大,可推测若numbers含有4,则必出现在mid所指元素的前面。此时,可以忽略mid后面的元素不再查找,并把mid赋给end即可。

另一种情况,如果*mid比4小,则要找的元素必出现在mid所指元素的后面。此时,通过令beg指向mid的下一个位置即可改变搜索范围。因为已经验证过mid不是我们要找的对象,所以在接下来的搜索中不必考虑它。

循环过程终止时,mid或者等于end或者指向要找的元素。如果mid等于end,说明numbers中没有我们要找的元素。

练习题

1 相邻元素的和

题目描述:
编写一个程序,读取一组整数到一个 std::vector 中,并打印每对相邻元素的和。例如,给定输入 1 2 3 4,输出应为 3 5 7。

代码示例:

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

int main() {
std::vector<int> numbers;
int num;

std::cout << "请输入一组整数(以 -1 结束): ";
while (std::cin >> num && num != -1) {
numbers.push_back(num);
}

std::cout << "相邻元素的和: ";
for (auto it = numbers.begin(); it + 1 != numbers.end(); ++it) {
std::cout << (*it + *(it + 1)) << " ";
}
std::cout << std::endl;

return 0;
}

答案:

  • 输入示例:1 2 3 4 -1
  • 输出示例:相邻元素的和: 3 5 7

2 反向打印

描述: 编写一个程序,从用户输入一组整数到一个 std::vector 中,然后使用迭代器反向打印这些元素。

代码:

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

int main() {
std::vector<int> numbers;
int input;

std::cout << "请输入一组整数(输入-1结束输入):\n";
while (std::cin >> input && input != -1) {
numbers.push_back(input);
}

std::cout << "反向打印结果:";
for (std::vector<int>::reverse_iterator it = numbers.rbegin(); it != numbers.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

return 0;
}

示例输入:

1
1 2 3 4 5 -1

示例输出:

1
反向打印结果:5 4 3 2 1 

3 合并两个 vector

描述: 编写一个程序,创建两个 std::vector,从用户输入填充它们。使用迭代器将这两个 vector 合并为一个新 vector。

代码:

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>
#include <vector>

int main() {
std::vector<int> vector1, vector2, mergedVector;
int input;

std::cout << "请输入第一个向量的整数(输入-1结束输入):\n";
while (std::cin >> input && input != -1) {
vector1.push_back(input);
}

std::cout << "请输入第二个向量的整数(输入-1结束输入):\n";
while (std::cin >> input && input != -1) {
vector2.push_back(input);
}

// 合并两个向量
mergedVector.insert(mergedVector.end(), vector1.begin(), vector1.end());
mergedVector.insert(mergedVector.end(), vector2.begin(), vector2.end());

std::cout << "合并后的向量结果:";
for (std::vector<int>::iterator it = mergedVector.begin(); it != mergedVector.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

return 0;
}

示例输入:

1
2
1 2 3 -1
4 5 6 -1

示例输出:

1
合并后的向量结果:1 2 3 4 5 6 

零基础C++(12) vector类用法

Posted on 2024-10-04 | In 零基础C++

1. 引言

什么是向量(Vector)?

向量(Vector)是 C++ 标准模板库(STL)中的一种序列容器,能够动态地管理可变大小的数组。与传统的固定大小的数组不同,向量可以根据需要随时调整其大小,提供更高的灵活性和便利性。

向量与数组的比较

特性 数组(Array) 向量(Vector)
大小 固定大小(编译时或运行时) 动态可变大小
内存管理 手动管理(需要预留足够空间) 自动管理(自动扩展或收缩)
支持的操作 限制较多 丰富的成员函数和操作
安全性 较低(易发生缓冲区溢出) 较高(通过成员函数进行边界检查)
与 STL 算法的兼容性 低 高

2. std::vector 基础

2.1 包含头文件

使用 std::vector 需要包含 <vector> 头文件:

1
#include <vector>

2.2 定义与初始化

定义一个整数向量:

1
std::vector<int> numbers;

定义一个字符串向量:

1
2
3
4
#include <vector>
#include <string>

std::vector<std::string> words;

初始化向量:

  • 默认初始化:

    1
    std::vector<int> vec1; // 空向量
  • 指定大小和默认值:

    1
    std::vector<int> vec2(5, 10); // 5个元素,值均为10
  • 使用初始化列表:

    1
    std::vector<int> vec3 = {1, 2, 3, 4, 5};
  • 拷贝构造:

    1
    std::vector<int> vec4(vec3); // 复制vec3
  • 移动构造:

    1
    std::vector<int> vec5(std::move(vec4)); // 移动vec4到vec5

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>

int main() {
// 默认初始化
std::vector<int> vec1;

// 指定大小和默认值
std::vector<int> vec2(5, 10);

// 使用初始化列表
std::vector<int> vec3 = {1, 2, 3, 4, 5};

// 拷贝构造
std::vector<int> vec4(vec3);

// 移动构造
std::vector<int> vec5(std::move(vec4));

// 输出vec2
std::cout << "vec2: ";
for(auto num : vec2) {
std::cout << num << " ";
}
std::cout << std::endl;

// 输出vec3
std::cout << "vec3: ";
for(auto num : vec3) {
std::cout << num << " ";
}
std::cout << std::endl;

// 输出vec5
std::cout << "vec5: ";
for(auto num : vec5) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1
2
3
vec2: 10 10 10 10 10 
vec3: 1 2 3 4 5
vec5: 1 2 3 4 5

2.3 向量的大小与容量

  • **size()**:返回向量中元素的数量。
  • **capacity()**:返回向量目前为止分配的存储容量。
  • **empty()**:检查向量是否为空。

示例代码:

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

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

std::cout << "Size: " << vec.size() << std::endl; // 输出: 3
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出: 3(或更大,取决于实现)

std::cout << "Is empty? " << (vec.empty() ? "Yes" : "No") << std::endl; // 输出: No

vec.reserve(10); // 预留容量
std::cout << "After reserve(10), Capacity: " << vec.capacity() << std::endl; // 输出: 10

vec.shrink_to_fit(); // 收缩到适合大小
std::cout << "After shrink_to_fit(), Capacity: " << vec.capacity() << std::endl; // 输出: 3

return 0;
}

输出示例:

1
2
3
4
5
Size: 3
Capacity: 3
Is empty? No
After reserve(10), Capacity: 10
After shrink_to_fit(), Capacity: 3

注意: capacity() 并不一定精确匹配 size(),它表示在需要重新分配内存之前,向量可以容纳的元素数量。


3. 向量的基本操作

3.1 添加与删除元素

  • **push_back()**:在向量末尾添加一个元素。
  • **pop_back()**:移除向量末尾的元素。
  • **insert()**:在指定位置插入元素。
  • **erase()**:移除指定位置的元素或范围内的元素。
  • **clear()**:移除所有元素。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec;

// 使用push_back添加元素
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);

std::cout << "After push_back: ";
for(auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 10 20 30

// 使用pop_back移除最后一个元素
vec.pop_back();

std::cout << "After pop_back: ";
for(auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 10 20

// 在第二个位置插入25
vec.insert(vec.begin() + 1, 25);

std::cout << "After insert: ";
for(auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 10 25 20

// 删除第二个元素(25)
vec.erase(vec.begin() + 1);

std::cout << "After erase: ";
for(auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 10 20

// 清空向量
vec.clear();
std::cout << "After clear, size: " << vec.size() << std::endl; // 输出: 0

return 0;
}

输出:

1
2
3
4
5
After push_back: 10 20 30 
After pop_back: 10 20
After insert: 10 25 20
After erase: 10 20
After clear, size: 0

3.2 访问元素

  • **operator[]**:通过索引访问元素。
  • **at()**:通过索引访问元素,带边界检查。
  • **front()**:访问第一个元素。
  • **back()**:访问最后一个元素。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>

int main() {
std::vector<std::string> fruits = {"Apple", "Banana", "Cherry"};

// 使用operator[]访问元素
std::cout << "First fruit: " << fruits[0] << std::endl; // 输出: Apple

// 使用at()访问元素
try {
std::cout << "Second fruit: " << fruits.at(1) << std::endl; // 输出: Banana
std::cout << "Invalid fruit: " << fruits.at(5) << std::endl; // 抛出异常
}
catch(const std::out_of_range& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}

// 使用front()和back()
std::cout << "Front: " << fruits.front() << std::endl; // 输出: Apple
std::cout << "Back: " << fruits.back() << std::endl; // 输出: Cherry

return 0;
}

输出:

1
2
3
4
5
First fruit: Apple
Second fruit: Banana
Exception: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
Front: Apple
Back: Cherry

3.3 遍历向量

  • 使用范围 for 循环
  • 使用传统 for 循环
  • 使用迭代器

示例代码:

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>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用范围 for 循环
std::cout << "Using range-based for loop: ";
for(auto num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// 使用传统 for 循环
std::cout << "Using traditional for loop: ";
for(size_t i = 0; i < numbers.size(); ++i) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;

// 使用迭代器
std::cout << "Using iterators: ";
for(auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1
2
3
Using range-based for loop: 1 2 3 4 5 
Using traditional for loop: 1 2 3 4 5
Using iterators: 1 2 3 4 5

3.4 修改元素

  • 通过索引或迭代器修改
  • 使用 assign() 重新赋值
  • 替换整个向量内容

示例代码:

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
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};

// 通过索引修改元素
vec[2] = 35;

// 使用 at() 修改元素
vec.at(4) = 55;

// 使用迭代器修改元素
for(auto it = vec.begin(); it != vec.end(); ++it) {
if(*it == 20) {
*it = 25;
}
}

// 输出修改后的向量
std::cout << "Modified vector: ";
for(auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 10 25 35 40 55

return 0;
}

输出:

1
Modified vector: 10 25 35 40 55 

4. 向量的高级用法

4.1 嵌套向量(二维向量)

向量可以包含其他向量,形成多维数组结构。

示例代码:二维向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <vector>

int main() {
// 定义一个3x4的二维向量,初始化为0
std::vector<std::vector<int>> matrix(3, std::vector<int>(4, 0));

// 填充矩阵
for(int i = 0; i < 3; ++i) {
for(int j = 0; j < 4; ++j) {
matrix[i][j] = i * 4 + j + 1;
}
}

// 输出矩阵
std::cout << "Matrix:" << std::endl;
for(auto row : matrix) {
for(auto elem : row) {
std::cout << elem << "\t";
}
std::cout << std::endl;
}

return 0;
}

输出:

1
2
3
4
Matrix:
1 2 3 4
5 6 7 8
9 10 11 12

4.2 向量与其他数据结构结合

向量可以与结构体、类等其他数据结构结合使用,增强数据组织能力。

示例代码:向量与结构体结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <vector>
#include <string>

// 定义学生结构体
struct Student {
int id;
std::string name;
float grade;
};

int main() {
// 定义一个学生向量
std::vector<Student> students;

// 添加学生
students.push_back({1001, "Alice", 89.5});
students.push_back({1002, "Bob", 92.0});
students.push_back({1003, "Charlie", 85.0});

// 遍历并输出学生信息
for(const auto& student : students) {
std::cout << "ID: " << student.id
<< ", Name: " << student.name
<< ", Grade: " << student.grade << std::endl;
}

return 0;
}

输出:

1
2
3
ID: 1001, Name: Alice, Grade: 89.5
ID: 1002, Name: Bob, Grade: 92
ID: 1003, Name: Charlie, Grade: 85

4.3 使用迭代器操作向量

迭代器是一种指针类型,用于遍历和操作容器中的元素。

示例代码:使用迭代器

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

int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};

// 使用迭代器遍历并修改元素
for(auto it = vec.begin(); it != vec.end(); ++it) {
*it += 5;
}

// 输出修改后的向量
std::cout << "After modifying: ";
for(auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl; // 输出: 15 25 35 45 55

return 0;
}

输出:

1
After modifying: 15 25 35 45 55 

5. 常用算法与向量

5.1 排序

可以使用 <algorithm> 头文件中的 sort() 函数对向量进行排序。

示例代码:对整数向量排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {50, 20, 40, 10, 30};

// 排序前
std::cout << "Before sorting: ";
for(auto num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// 使用sort()排序
std::sort(numbers.begin(), numbers.end());

// 排序后
std::cout << "After sorting: ";
for(auto num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1
2
Before sorting: 50 20 40 10 30 
After sorting: 10 20 30 40 50

自定义排序规则:降序

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

int main() {
std::vector<int> numbers = {50, 20, 40, 10, 30};

// 使用sort()并传入lambda表达式进行降序排序
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b;
});

// 输出排序后的向量
std::cout << "After sorting in descending order: ";
for(auto num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1
After sorting in descending order: 50 40 30 20 10 

5.2 反转

使用 reverse() 函数可以反转向量中的元素顺序。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<char> letters = {'A', 'B', 'C', 'D', 'E'};

std::cout << "Before reversing: ";
for(auto c : letters) {
std::cout << c << " ";
}
std::cout << std::endl;

// 反转向量
std::reverse(letters.begin(), letters.end());

std::cout << "After reversing: ";
for(auto c : letters) {
std::cout << c << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1
2
Before reversing: A B C D E 
After reversing: E D C B A

5.3 查找

使用 find() 函数可以在向量中查找特定元素。

示例代码:

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

int main() {
std::vector<std::string> fruits = {"Apple", "Banana", "Cherry", "Date"};
std::string target = "Cherry";

// 使用find()查找元素
auto it = std::find(fruits.begin(), fruits.end(), target);

if(it != fruits.end()) {
std::cout << target << " found at position " << std::distance(fruits.begin(), it) << std::endl;
}
else {
std::cout << target << " not found." << std::endl;
}

return 0;
}

输出:

1
Cherry found at position 2

6. 向量的性能与优化

6.1 内存管理

向量会动态地管理内存,自动调整其容量以适应新增或删除的元素。频繁的内存分配可能会影响性能。

6.2 预留空间

使用 reserve() 可以提前为向量分配足够的内存,减少内存重新分配的次数,提高性能。

示例代码:

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

int main() {
std::vector<int> vec;

// 预留空间
vec.reserve(1000);
std::cout << "Capacity after reserve(1000): " << vec.capacity() << std::endl;

// 添加元素
for(int i = 0; i < 1000; ++i) {
vec.push_back(i);
}

std::cout << "Size after adding elements: " << vec.size() << std::endl;
std::cout << "Capacity after adding elements: " << vec.capacity() << std::endl;

return 0;
}

输出示例:

1
2
3
Capacity after reserve(1000): 1000
Size after adding elements: 1000
Capacity after adding elements: 1000

6.3 收缩容量

使用 shrink_to_fit() 可以请求收缩向量的容量以匹配其大小,释放多余的内存。

示例代码:

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

int main() {
std::vector<int> vec;

// 预留较大的空间
vec.reserve(1000);
std::cout << "Capacity before adding: " << vec.capacity() << std::endl;

// 添加少量元素
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
std::cout << "Size after adding: " << vec.size() << std::endl;
std::cout << "Capacity after adding: " << vec.capacity() << std::endl;

// 收缩容量
vec.shrink_to_fit();
std::cout << "Capacity after shrink_to_fit: " << vec.capacity() << std::endl;

return 0;
}

输出示例:

1
2
3
4
Capacity before adding: 1000
Size after adding: 3
Capacity after adding: 1000
Capacity after shrink_to_fit: 3

7. 示例项目

示例项目1:学生信息管理系统

需求分析:

创建一个程序,管理学生的信息,包括添加、删除、显示和查找学生。每个学生包含ID、姓名和成绩。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// 定义学生结构体
struct Student {
int id;
std::string name;
float grade;
};

// 打印学生信息
void printStudent(const Student& student) {
std::cout << "ID: " << student.id
<< ", Name: " << student.name
<< ", Grade: " << student.grade << std::endl;
}

// 添加学生
void addStudent(std::vector<Student>& students) {
Student s;
std::cout << "Enter Student ID: ";
std::cin >> s.id;
std::cout << "Enter Student Name: ";
std::cin.ignore(); // 忽略之前输入的换行符
std::getline(std::cin, s.name);
std::cout << "Enter Student Grade: ";
std::cin >> s.grade;
students.push_back(s);
std::cout << "Student added successfully.\n";
}

// 删除学生
void deleteStudent(std::vector<Student>& students) {
int id;
std::cout << "Enter Student ID to delete: ";
std::cin >> id;

auto it = std::find_if(students.begin(), students.end(), [id](const Student& s) {
return s.id == id;
});

if(it != students.end()) {
students.erase(it);
std::cout << "Student deleted successfully.\n";
}
else {
std::cout << "Student with ID " << id << " not found.\n";
}
}

// 显示所有学生
void displayStudents(const std::vector<Student>& students) {
if(students.empty()) {
std::cout << "No students available.\n";
return;
}
std::cout << "Student List:\n";
for(const auto& s : students) {
printStudent(s);
}
}

// 查找学生
void findStudent(const std::vector<Student>& students) {
int id;
std::cout << "Enter Student ID to find: ";
std::cin >> id;

auto it = std::find_if(students.begin(), students.end(), [id](const Student& s) {
return s.id == id;
});

if(it != students.end()) {
std::cout << "Student Found:\n";
printStudent(*it);
}
else {
std::cout << "Student with ID " << id << " not found.\n";
}
}

int main() {
std::vector<Student> students;
int choice;

do {
std::cout << "\n=== Student Management System ===\n";
std::cout << "1. Add Student\n";
std::cout << "2. Delete Student\n";
std::cout << "3. Display All Students\n";
std::cout << "4. Find Student by ID\n";
std::cout << "5. Exit\n";
std::cout << "Enter your choice (1-5): ";
std::cin >> choice;

switch(choice) {
case 1:
addStudent(students);
break;
case 2:
deleteStudent(students);
break;
case 3:
displayStudents(students);
break;
case 4:
findStudent(students);
break;
case 5:
std::cout << "Exiting the system.\n";
break;
default:
std::cout << "Invalid choice. Please choose between 1-5.\n";
}

} while(choice != 5);

return 0;
}

运行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
=== Student Management System ===
1. Add Student
2. Delete Student
3. Display All Students
4. Find Student by ID
5. Exit
Enter your choice (1-5): 1
Enter Student ID: 1001
Enter Student Name: Alice
Enter Student Grade: 89.5
Student added successfully.

=== Student Management System ===
1. Add Student
2. Delete Student
3. Display All Students
4. Find Student by ID
5. Exit
Enter your choice (1-5): 1
Enter Student ID: 1002
Enter Student Name: Bob
Enter Student Grade: 92
Student added successfully.

=== Student Management System ===
1. Add Student
2. Delete Student
3. Display All Students
4. Find Student by ID
5. Exit
Enter your choice (1-5): 3
Student List:
ID: 1001, Name: Alice, Grade: 89.5
ID: 1002, Name: Bob, Grade: 92

=== Student Management System ===
1. Add Student
2. Delete Student
3. Display All Students
4. Find Student by ID
5. Exit
Enter your choice (1-5): 4
Enter Student ID to find: 1001
Student Found:
ID: 1001, Name: Alice, Grade: 89.5

=== Student Management System ===
1. Add Student
2. Delete Student
3. Display All Students
4. Find Student by ID
5. Exit
Enter your choice (1-5): 5
Exiting the system.

代码解析:

  1. 结构体定义: 定义了一个 Student 结构体,包含 id、name 和 grade。
  2. 功能函数:
    • addStudent:添加新学生。
    • deleteStudent:根据 ID 删除学生。
    • displayStudents:显示所有学生的信息。
    • findStudent:根据 ID 查找并显示学生信息。
  3. 主函数: 提供一个菜单驱动的用户界面,允许用户选择不同的操作。

示例项目2:动态库存管理系统

需求分析:

创建一个程序,管理库存中的商品信息,包括添加、删除、更新和显示商品。每个商品包含商品ID、名称和数量。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// 定义商品结构体
struct Product {
int id;
std::string name;
int quantity;
};

// 打印商品信息
void printProduct(const Product& product) {
std::cout << "ID: " << product.id
<< ", Name: " << product.name
<< ", Quantity: " << product.quantity << std::endl;
}

// 添加商品
void addProduct(std::vector<Product>& products) {
Product p;
std::cout << "Enter Product ID: ";
std::cin >> p.id;
std::cout << "Enter Product Name: ";
std::cin.ignore(); // 忽略之前输入的换行符
std::getline(std::cin, p.name);
std::cout << "Enter Product Quantity: ";
std::cin >> p.quantity;
products.push_back(p);
std::cout << "Product added successfully.\n";
}

// 删除商品
void deleteProduct(std::vector<Product>& products) {
int id;
std::cout << "Enter Product ID to delete: ";
std::cin >> id;

auto it = std::find_if(products.begin(), products.end(), [id](const Product& p) {
return p.id == id;
});

if(it != products.end()) {
products.erase(it);
std::cout << "Product deleted successfully.\n";
}
else {
std::cout << "Product with ID " << id << " not found.\n";
}
}

// 更新商品数量
void updateProductQuantity(std::vector<Product>& products) {
int id, newQty;
std::cout << "Enter Product ID to update: ";
std::cin >> id;

auto it = std::find_if(products.begin(), products.end(), [id](const Product& p) {
return p.id == id;
});

if(it != products.end()) {
std::cout << "Enter new quantity: ";
std::cin >> newQty;
it->quantity = newQty;
std::cout << "Product quantity updated successfully.\n";
}
else {
std::cout << "Product with ID " << id << " not found.\n";
}
}

// 显示所有商品
void displayProducts(const std::vector<Product>& products) {
if(products.empty()) {
std::cout << "No products available.\n";
return;
}
std::cout << "Product List:\n";
for(const auto& p : products) {
printProduct(p);
}
}

int main() {
std::vector<Product> products;
int choice;

do {
std::cout << "\n=== Inventory Management System ===\n";
std::cout << "1. Add Product\n";
std::cout << "2. Delete Product\n";
std::cout << "3. Update Product Quantity\n";
std::cout << "4. Display All Products\n";
std::cout << "5. Exit\n";
std::cout << "Enter your choice (1-5): ";
std::cin >> choice;

switch(choice) {
case 1:
addProduct(products);
break;
case 2:
deleteProduct(products);
break;
case 3:
updateProductQuantity(products);
break;
case 4:
displayProducts(products);
break;
case 5:
std::cout << "Exiting the system.\n";
break;
default:
std::cout << "Invalid choice. Please choose between 1-5.\n";
}

} while(choice != 5);

return 0;
}

运行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 1
Enter Product ID: 2001
Enter Product Name: Laptop
Enter Product Quantity: 50
Product added successfully.

=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 1
Enter Product ID: 2002
Enter Product Name: Smartphone
Enter Product Quantity: 150
Product added successfully.

=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 4
Product List:
ID: 2001, Name: Laptop, Quantity: 50
ID: 2002, Name: Smartphone, Quantity: 150

=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 3
Enter Product ID to update: 2001
Enter new quantity: 45
Product quantity updated successfully.

=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 4
Product List:
ID: 2001, Name: Laptop, Quantity: 45
ID: 2002, Name: Smartphone, Quantity: 150

=== Inventory Management System ===
1. Add Product
2. Delete Product
3. Update Product Quantity
4. Display All Products
5. Exit
Enter your choice (1-5): 5
Exiting the system.

代码解析:

  1. 结构体定义: 定义了一个 Product 结构体,包含 id、name 和 quantity。
  2. 功能函数:
    • addProduct:添加新商品。
    • deleteProduct:根据 ID 删除商品。
    • updateProductQuantity:根据 ID 更新商品数量。
    • displayProducts:显示所有商品的信息。
  3. 主函数: 提供一个菜单驱动的用户界面,允许用户选择不同的操作。

零基础C++(11) string类用法

Posted on 2024-10-03 | In 零基础C++

1. 引言

什么是字符串?

字符串是由一系列字符组成的序列,用于表示文本信息。它在编程中被广泛应用于用户交互、文件处理、数据解析等场景。

C 风格字符串 vs std::string

在 C++ 中,有两种主要的字符串类型:

  • C 风格字符串(C-strings):基于字符数组,以空字符 ('\0') 结尾。
  • C++ std::string 类:更高级、功能更丰富的字符串类,封装了字符串操作的复杂性。

C 风格字符串示例:

1
char cstr[] = "Hello, World!";

std::string 示例:

1
2
3
#include <string>

std::string str = "Hello, World!";

2. std::string 基础

定义与初始化

std::string 是 C++ 标准库中的一个类,位于 <string> 头文件中。它封装了字符序列,并提供了丰富的成员函数用于操作字符串。

初始化有很多中方式,如下图

https://cdn.llfc.club/1727932857214.jpg

包含头文件:

1
#include <string>

初始化示例:

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
#include <iostream>
#include <string>

int main() {
// 默认构造函数
std::string str1;

// 使用字符串字面值初始化
std::string str2 = "Hello";

// 使用拷贝构造函数
std::string str3(str2);

// 使用部分初始化
std::string str4(str2, 0, 3); // "Hel"

// 使用重复字符初始化
std::string str5(5, 'A'); // "AAAAA"

std::cout << "str1: " << str1 << std::endl;
std::cout << "str2: " << str2 << std::endl;
std::cout << "str3: " << str3 << std::endl;
std::cout << "str4: " << str4 << std::endl;
std::cout << "str5: " << str5 << std::endl;

return 0;
}

输出:

1
2
3
4
5
str1:
str2: Hello
str3: Hello
str4: Hel
str5: AAAAA

字符串输入与输出

输出字符串:

1
2
3
4
5
6
7
8
#include <iostream>
#include <string>

int main() {
std::string greeting = "Hello, C++ Strings!";
std::cout << greeting << std::endl;
return 0;
}

从用户输入字符串:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main() {
std::string input;
std::cout << "请输入一个字符串:";
std::cin >> input; // 读取直到第一个空白字符
std::cout << "您输入的字符串是:" << input << std::endl;
return 0;
}

读取包含空格的整行字符串:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main() {
std::string line;
std::cout << "请输入一行文本:";
std::getline(std::cin, line);
std::cout << "您输入的文本是:" << line << std::endl;
return 0;
}

3. 字符串操作

常用的字符串操作如下:

https://cdn.llfc.club/1727933166315.jpg

3.1 拼接与连接

使用 + 运算符:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main() {
std::string first = "Hello, ";
std::string second = "World!";
std::string combined = first + second;
std::cout << combined << std::endl; // 输出: Hello, World!
return 0;
}

使用 append() 函数:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
std::string str = "Hello";
str.append(", World!");
std::cout << str << std::endl; // 输出: Hello, World!
return 0;
}

使用 += 运算符:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
std::string str = "Data";
str += " Structures";
std::cout << str << std::endl; // 输出: Data Structures
return 0;
}

3.2 比较字符串

关于字符串的比较,其实是逐个位置按照字符比较,计算机中字符存储的方式是ASCII码表,每个字符对应一个ASCII码值,比较字符就是比较ASCII码值的大小

https://cdn.llfc.club/ascii-1-3.png

一些控制字符也是通过ASCII码存储的

https://cdn.llfc.club/ascii-2-1.png

使用 ==, !=, <, >, <=, >= 运算符:

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

int main() {
std::string a = "apple";
std::string b = "banana";

if (a == b) {
std::cout << "a 和 b 相等" << std::endl;
} else {
std::cout << "a 和 b 不相等" << std::endl;
}

if (a < b) {
std::cout << "a 在字典序中小于 b" << std::endl;
} else {
std::cout << "a 在字典序中不小于 b" << std::endl;
}

return 0;
}

输出:

1
2
a 和 b 不相等
a 在字典序中小于 b

3.3 查找与替换

使用 find() 查找子字符串:

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

int main() {
std::string text = "The quick brown fox jumps over the lazy dog.";
std::string word = "fox";

size_t pos = text.find(word);
if (pos != std::string::npos) {
std::cout << "找到 '" << word << "' 在位置: " << pos << std::endl;
} else {
std::cout << "'" << word << "' 未找到。" << std::endl;
}

return 0;
}

替换子字符串:

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

int main() {
std::string text = "I like cats.";
std::string from = "cats";
std::string to = "dogs";

size_t pos = text.find(from);
if (pos != std::string::npos) {
text.replace(pos, from.length(), to);
std::cout << "替换后: " << text << std::endl; // 输出: I like dogs.
} else {
std::cout << "'" << from << "' 未找到。" << std::endl;
}

return 0;
}

3.4 子字符串与切片

使用 substr() 获取子字符串:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
std::string str = "Hello, World!";
std::string sub = str.substr(7, 5); // 从位置7开始,长度5
std::cout << sub << std::endl; // 输出: World
return 0;
}

注意: 如果省略第二个参数,substr() 会返回从起始位置到字符串末尾的所有字符。

1
2
std::string sub = str.substr(7); // 从位置7开始直到结束
std::cout << sub << std::endl; // 输出: World!

4. 字符串的常用成员函数

4.1 长度与容量

获取字符串长度:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main() {
std::string str = "C++ Programming";
std::cout << "字符串长度: " << str.length() << std::endl; // 输出: 14
// 或者使用 size()
std::cout << "字符串大小: " << str.size() << std::endl; // 输出: 14
return 0;
}

获取字符串容量:

每个 std::string 对象都有一个容量(capacity),表示它当前能够持有的最大字符数,而不需要重新分配内存。

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

int main() {
std::string str = "Hello";
std::cout << "初始容量: " << str.capacity() << std::endl;

str += ", World!";
std::cout << "追加后的容量: " << str.capacity() << std::endl;

return 0;
}

输出示例:

1
2
初始容量: 15
追加后的容量: 15

注意: 容量可能因实现而异,并不保证它等于长度。

4.2 访问字符

对字符串中的字符操作,有如下方法, 切记需包含头文件

https://cdn.llfc.club/1727933254985.jpg

使用索引访问单个字符:

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

int main() {
std::string str = "ABCDE";

// 正向索引
for (size_t i = 0; i < str.length(); ++i) {
std::cout << "字符 " << i << ": " << str[i] << std::endl;
}

//反向遍历
for(int i = str.length() - 1; i >= 0 ; i --){
std::cout << "下标为 " << i << "的字符为" << str[i] << std::endl;
}

return 0;
}

使用 at() 函数(包含边界检查):

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

int main() {
std::string str = "ABCDE";
try {
char c = str.at(10); // 超出范围,会抛出异常
} catch (const std::out_of_range& e) {
std::cout << "异常捕获: " << e.what() << std::endl;
}
return 0;
}

输出:

1
异常捕获: basic_string::at: __n (which is 10) >= this->size() (which is 5)

4.3 转换大小写

C++ 标准库中的 std::toupper 和 std::tolower 可以用于转换字符的大小写。结合 std::transform,可以实现整个字符串的大小写转换。

转换为大写:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

int main() {
std::string str = "Hello, World!";
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::toupper(c); });
std::cout << str << std::endl; // 输出: HELLO, WORLD!
return 0;
}

转换为小写:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

int main() {
std::string str = "Hello, World!";
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::tolower(c); });
std::cout << str << std::endl; // 输出: hello, world!
return 0;
}

4.4 其他有用的函数

  • **empty()**:检查字符串是否为空。

    1
    2
    3
    4
    std::string str;
    if (str.empty()) {
    std::cout << "字符串为空。" << std::endl;
    }
  • **clear()**:清空字符串内容。

    1
    2
    3
    std::string str = "Clear me!";
    str.clear();
    std::cout << "str: " << str << std::endl; // 输出为空
  • **erase()**:删除字符串的部分内容。

    1
    2
    3
    std::string str = "Hello, World!";
    str.erase(5, 7); // 从位置5开始,删除7个字符
    std::cout << str << std::endl; // 输出: Hello!
  • **insert()**:在指定位置插入字符串或字符。

    1
    2
    3
    std::string str = "Hello World";
    str.insert(5, ",");
    std::cout << str << std::endl; // 输出: Hello, World
  • **replace()**:替换字符串的部分内容(前面已示例)。

  • **find_first_of(), find_last_of()**:查找字符集合中的任何一个字符。

    1
    2
    3
    std::string str = "apple, banana, cherry";
    size_t pos = str.find_first_of(", ");
    std::cout << "第一个逗号或空格的位置: " << pos << std::endl; // 输出: 5

5. 高级用法

5.1 字符串流(stringstream)

std::stringstream 是 C++ 标准库中第 <sstream> 头文件提供的一个类,用于在内存中进行字符串的读写操作,类似于文件流。

基本用法示例:

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

int main() {
std::stringstream ss;
ss << "Value: " << 42 << ", " << 3.14;

std::string result = ss.str();
std::cout << result << std::endl; // 输出: Value: 42, 3.14

return 0;
}

从字符串流中读取数据:

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

int main() {
std::string data = "123 45.67 Hello";
std::stringstream ss(data);

int a;
double b;
std::string c;

ss >> a >> b >> c;

std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
// 输出: a: 123, b: 45.67, c: Hello

return 0;
}

5.2 字符串与其他数据类型的转换

将其他类型转换为 std::string:

  • 使用 std::to_string():

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

    int main() {
    int num = 100;
    double pi = 3.14159;

    std::string str1 = std::to_string(num);
    std::string str2 = std::to_string(pi);

    std::cout << "str1: " << str1 << ", str2: " << str2 << std::endl;
    // 输出: str1: 100, str2: 3.141590
    return 0;
    }

将 std::string 转换为其他类型:

  • 使用字符串流:

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

    int main() {
    std::string numStr = "256";
    std::string piStr = "3.14";

    int num;
    double pi;

    std::stringstream ss1(numStr);
    ss1 >> num;

    std::stringstream ss2(piStr);
    ss2 >> pi;

    std::cout << "num: " << num << ", pi: " << pi << std::endl;
    // 输出: num: 256, pi: 3.14
    return 0;
    }
  • 使用 std::stoi(), std::stod() 等函数(C++11 及以上):

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

    int main() {
    std::string numStr = "256";
    std::string piStr = "3.14";

    int num = std::stoi(numStr);
    double pi = std::stod(piStr);

    std::cout << "num: " << num << ", pi: " << pi << std::endl;
    // 输出: num: 256, pi: 3.14
    return 0;
    }

5.3 正则表达式与字符串匹配

C++ 标准库提供了 <regex> 头文件,用于支持正则表达式。

关于正则表达式的规则可以参考菜鸟教程文档https://www.runoob.com/regexp/regexp-syntax.html

基本用法示例:

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

int main() {
std::string text = "The quick brown fox jumps over the lazy dog.";
std::regex pattern(R"(\b\w{5}\b)"); // 匹配所有5个字母的单词

std::sregex_iterator it(text.begin(), text.end(), pattern);
std::sregex_iterator end;

std::cout << "5个字母的单词有:" << std::endl;
while (it != end) {
std::cout << (*it).str() << std::endl;
++it;
}

return 0;
}

输出:

1
2
3
4
5
5个字母的单词有:
quick
brown
jumps
leazy

说明:

  • \b 匹配单词边界。
  • \w{5} 匹配恰好5个字母的单词。

注意: 使用原始字符串字面值(R"()")以简化正则表达式的编写。


6. 字符串与 C 风格字符串的转换

6.1 从 C 风格字符串转换为 std::string

通过 std::string 的构造函数,可以轻松将 C 风格字符串转换为 std::string。

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
const char* cstr = "Hello, C-strings!";
std::string str(cstr);
std::cout << str << std::endl; // 输出: Hello, C-strings!
return 0;
}

6.2 从 std::string 转换为 C 风格字符串

使用 c_str() 成员函数,可以获取 C 风格字符串指针。

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
std::string str = "Hello, std::string!";
const char* cstr = str.c_str();
std::cout << cstr << std::endl; // 输出: Hello, std::string!
return 0;
}

注意: 返回的指针是只读的,且指向的内存由 std::string 管理,确保在 std::string 对象有效期间使用。


7. 示例项目

示例项目1:简易文本分析器

需求分析:

创建一个程序,接受用户输入的一段文本,并提供以下功能:

  • 统计单词数量
  • 统计每个单词出现的次数
  • 查找指定单词的出现次数
  • 输出最长的单词

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <string>
#include <sstream>
#include <map>
#include <algorithm>

int main() {
std::string text;
std::cout << "请输入一段文本(结束请输入Ctrl+D/Ctrl+Z):\n";

// 读取整段文本
std::ostringstream oss;
std::string line;
while (std::getline(std::cin, line)) {
oss << line << " ";
}
text = oss.str();

// 使用字符串流分割单词
std::stringstream ss(text);
std::string word;
std::map<std::string, int> wordCount;
size_t totalWords = 0;
std::string longestWord;

while (ss >> word) {
// 去除标点符号(简单处理)
word.erase(std::remove_if(word.begin(), word.end(),
[](char c) { return ispunct(c); }), word.end());

// 转为小写
std::transform(word.begin(), word.end(), word.begin(), ::tolower);

if (!word.empty()) {
wordCount[word]++;
totalWords++;
if (word.length() > longestWord.length()) {
longestWord = word;
}
}
}

std::cout << "\n统计结果:\n";
std::cout << "总单词数: " << totalWords << std::endl;
std::cout << "每个单词出现的次数:\n";
for (const auto& pair : wordCount) {
std::cout << pair.first << ": " << pair.second << std::endl;
}

std::cout << "最长的单词: " << longestWord << std::endl;

// 查找指定单词的出现次数
std::string searchWord;
std::cout << "\n请输入要查找的单词: ";
std::cin >> searchWord;
// 转为小写
std::transform(searchWord.begin(), searchWord.end(), searchWord.begin(), ::tolower);
auto it = wordCount.find(searchWord);
if (it != wordCount.end()) {
std::cout << "'" << searchWord << "' 出现了 " << it->second << " 次。" << std::endl;
} else {
std::cout << "'" << searchWord << "' 未在文本中找到。" << std::endl;
}

return 0;
}

运行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
请输入一段文本(结束请输入Ctrl+D/Ctrl+Z):
Hello, world! Hello C++.
This is a simple text analyzer. Analyzing text is fun.

统计结果:
总单词数: 10
每个单词出现的次数:
hello: 2
world: 1
c: 1
this: 1
is: 2
a: 1
simple: 1
text: 2
analyzer: 1
analyzing: 1
fun: 1
最长的单词: analyzer

请输入要查找的单词: text
'text' 出现了 2 次。

代码解析:

  1. 读取用户输入的文本:使用 std::ostringstream 和 std::getline 读取用户输入的多行文本,直到用户输入结束(Ctrl+D 或 Ctrl+Z)。
  2. 分割单词并统计:
    • 使用 std::stringstream 将文本分割为单词。
    • 使用 std::map 存储每个单词出现的次数。
    • 计算总单词数和最长单词。
  3. 查找指定单词:用户输入要查找的单词,程序查找并输出出现次数。

示例项目2:用户输入验证工具

需求分析:

编写一个程序,接受用户输入的电子邮件地址,并验证其格式是否正确。简单的验证标准:

  • 包含一个 @ 符号
  • @ 后面有一个 . 符号
  • 不包含空格

代码实现:

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

bool isValidEmail(const std::string& email) {
// 简单的正则表达式,匹配基本的邮件格式
const std::regex pattern(R"((\w+)(\.?\w+)*@(\w+)(\.\w+)+)");
return std::regex_match(email, pattern);
}

int main() {
std::string email;
std::cout << "请输入您的电子邮件地址: ";
std::cin >> email;

if (isValidEmail(email)) {
std::cout << "电子邮件地址有效。" << std::endl;
} else {
std::cout << "电子邮件地址无效。" << std::endl;
}

return 0;
}

运行示例:

1
2
请输入您的电子邮件地址: user@example.com
电子邮件地址有效。
1
2
请输入您的电子邮件地址: userexample.com
电子邮件地址无效。

代码解析:

  1. **定义验证函数 isValidEmail**:
    • 使用正则表达式 (\w+)(\.?\w+)*@(\w+)(\.\w+)+ 来匹配基本的邮箱格式。
    • 该正则表达式匹配如下部分:
      • 用户名部分:由字母数字字符组成,可以包含点号。
      • @ 符号。
      • 域名部分:由字母数字字符组成,至少包含一个点号后跟字母数字字符。
  2. 主函数:
    • 提示用户输入邮箱地址。
    • 调用 isValidEmail 函数进行验证,并输出结果。

注意: 这个正则表达式只是一个基础的验证,实际应用中可能需要更复杂的正则表达式来处理更多的邮件格式。


零基础C++(10) 命名空间

Posted on 2024-10-03 | In 零基础C++

命名空间的using声明

目前为止,我们用到的库函数基本上都属于命名空间std,而程序也显式地将这一点标示了出来。

例如,std::cin表示从标准输入中读取内容。此处使用作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。

因此,std::cin的意思就是要使用命名空间std中的名字cin。

上面的方法显得比较烦琐,然而幸运的是,通过更简单的途径也能使用到命名空间中的成员。

本节将学习其中一种简单的方法,使用using声明(using declaration),有了using声明就无须专门的前缀(形如命名空间::)也能使用所需的名字了。using声明具有如下的形式:

1
using namespace::name;

一旦声明了上述语句,就可以直接访问命名空间中的名字:

1
2
3
4
5
6
7
8
9
10
11
using std::cin;
int main() {
int i ;
//正确,cin和std::cin含义相同
cin >> i;
//错误,没有对应的using声明,必须使用完整的名字
//cout << i;
//正确,显示地从std中使用cout
std::cout << i;
return 0;
}

每个名字都需要独立的using声明

按照规定,每个using声明引入命名空间中的一个成员。例如,可以把要用到的标准库中的名字都以using声明的形式表示出来,程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
using std::cin;
using std::endl;
int main() {
int i ;
//正确,cin和std::cin含义相同
cin >> i;
//错误,没有对应的using声明,必须使用完整的名字
//cout << i;
//正确,显示地从std中使用cout
std::cout << i << endl;
return 0;
}

头文件不应包含using声明

位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

零基础C++(9) 结构体类型

Posted on 2024-10-01 | In 零基础C++

什么是结构体?

结构体(structure,简称 struct)是一种用户自定义的数据类型,用于将多个不同类型的数据组合在一起。它允许在一个单一的单元中存储多个相关的数据项,使代码更具组织性和可读性。

结构体在编程中的应用场景

  • 数据组织:将相关的数据组合,如学生信息、坐标点、日期等。
  • 传递数据:在函数之间传递多个相关的数据项。
  • 复杂数据处理:管理更复杂的数据结构,如链表、树、图等。

结构体的基本使用

定义和声明结构体

在 C++ 中,使用 struct 关键字定义一个结构体。基本语法如下:

1
2
3
4
5
6
struct StructName {
// 成员变量
dataType1 member1;
dataType2 member2;
// ...
};

示例:定义一个学生结构体

1
2
3
4
5
struct Student {
int id;
std::string name;
float grade;
};

创建结构体变量

定义结构体后,可以创建结构体类型的变量。

1
2
Student student1;
Student student2;

访问结构体成员

使用点运算符(.)访问结构体成员。

1
2
3
4
5
6
7
student1.id = 1001;
student1.name = "Alice";
student1.grade = 89.5f;

std::cout << "学生ID: " << student1.id << std::endl;
std::cout << "学生姓名: " << student1.name << std::endl;
std::cout << "学生成绩: " << student1.grade << std::endl;

结构体初始化

可以在创建结构体变量时进行初始化。

方法一:直观初始化

1
Student student3 = {1002, "Bob", 92.0f};

方法二:逐个赋值

1
2
3
4
Student student4;
student4.id = 1003;
student4.name = "Charlie";
student4.grade = 85.0f;

方法三:使用自定义构造函数

虽然结构体的主要用途是数据存储,但在 C++ 中,结构体可以像类一样拥有构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
struct Student {
int id;
std::string name;
float grade;

// 构造函数
Student(int studentId, std::string studentName, float studentGrade) :
id(studentId), name(studentName), grade(studentGrade) {}
};

// 使用构造函数初始化
Student student5(1004, "Daisy", 95.0f);

结构体与类的比较

在 C++ 中,struct 和 class 有很多相似之处,但也有一些关键区别。

类与结构体的相似之处

  • 都可以包含成员变量和成员函数。
  • 都支持访问控制(public、protected、private)。
  • 都可以使用继承和多态。

类与结构体的区别

默认访问控制

  • 结构体(struct):默认成员访问权限为 public。
  • 类(class):默认成员访问权限为 private。

例子

1
2
3
4
5
6
7
struct StructExample {
int x; // 默认 public
};

class ClassExample {
int y; // 默认 private
};

用途习惯

  • 结构体(struct):通常用于纯数据结构,主要存储数据,成员通常是公开的。
  • 类(class):用于包含数据和操作数据的函数,支持更加复杂的封装。

实例比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Point {
int x;
int y;
};

class Rectangle {
private:
int width;
int height;

public:
void setDimensions(int w, int h) {
width = w;
height = h;
}

int area() const {
return width * height;
}

int get_width(){
return width;
}
};

int main(){
Rectangle rt;
//错误,不能直接访问私有成员
rt.width;
//正确,可以通过公有成员函数访问
rt.get_width();
}

结构体的高级用法

嵌套结构体

结构体可以包含其他结构体作为成员。

示例:嵌套地址结构体

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
struct Address {
std::string city;
std::string street;
int houseNumber;
};

struct Person {
std::string name;
int age;
Address address; // 嵌套结构体
};

int main() {
Person person;
person.name = "Eve";
person.age = 30;
person.address.city = "New York";
person.address.street = "5th Avenue";
person.address.houseNumber = 101;

std::cout << person.name << " lives at "
<< person.address.houseNumber << " "
<< person.address.street << ", "
<< person.address.city << std::endl;

return 0;
}

输出:

1
Eve lives at 101 5th Avenue, New York

结构体数组

可以创建包含多个结构体的数组,用于存储多个相同类型的数据项。

示例:存储多个学生信息的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Student {
int id;
std::string name;
float grade;
};

int main() {
// 创建包含3个学生信息的数组
Student students[3] = {
{1001, "Alice", 89.5f},
{1002, "Bob", 92.0f},
{1003, "Charlie", 85.0f}
};

for (int i = 0; i < 3; ++i) {
std::cout << "学生ID: " << students[i].id
<< ", 姓名: " << students[i].name
<< ", 成绩: " << students[i].grade << std::endl;
}

return 0;
}

输出:

1
2
3
学生ID: 1001, 姓名: Alice, 成绩: 89.5
学生ID: 1002, 姓名: Bob, 成绩: 92
学生ID: 1003, 姓名: Charlie, 成绩: 85

结构体指针

可以创建指向结构体的指针,并通过指针访问结构体成员。

示例:使用结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Car {
std::string brand;
std::string model;
int year;
};

int main() {
Car car = {"Toyota", "Camry", 2020};
Car* carPtr = &car;

std::cout << "品牌: " << carPtr->brand << std::endl;
std::cout << "型号: " << carPtr->model << std::endl;
std::cout << "年份: " << carPtr->year << std::endl;

return 0;
}

输出:

1
2
3
品牌: Toyota
型号: Camry
年份: 2020

使用 typedef 简化结构体定义

使用 typedef(或 using 关键字)可以为结构体类型创建别名,使代码更简洁。

示例:使用 typedef

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
int id;
std::string name;
float grade;
} Student;

// 或者使用 `using`(C++11 及以上)
using Student = struct {
int id;
std::string name;
float grade;
};

应用:

1
2
3
4
5
6
7
int main() {
Student s1 = {1004, "Daisy", 95.0f};
std::cout << "学生ID: " << s1.id
<< ", 姓名: " << s1.name
<< ", 成绩: " << s1.grade << std::endl;
return 0;
}

输出:

1
学生ID: 1004, 姓名: Daisy, 成绩: 95

5. 结构体中的函数

虽然结构体主要用于存储数据,但在 C++ 中,结构体也可以包含成员函数。这使得结构体更具面向对象的特性。

示例:在结构体中定义成员函数

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

struct Book {
std::string title;
std::string author;
int pages;

// 成员函数
void printInfo() const {
std::cout << "书名: " << title
<< ", 作者: " << author
<< ", 页数: " << pages << std::endl;
}
};

int main() {
Book myBook = {"C++ Primer", "Stanley B. Lippman", 976};
myBook.printInfo();
return 0;
}

输出:

1
书名: C++ Primer, 作者: Stanley B. Lippman, 页数: 976

使用结构体作为函数参数和返回值

传递结构体给函数

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
struct Point {
int x;
int y;
};

// 通过值传递
void printPoint(Point p) {
std::cout << "Point(" << p.x << ", " << p.y << ")" << std::endl;
}

// 通过引用传递
void movePoint(Point& p, int dx, int dy) {
p.x += dx;
p.y += dy;
}

int main() {
Point p1 = {10, 20};
printPoint(p1);

movePoint(p1, 5, -5);
printPoint(p1);

return 0;
}

输出:

1
2
Point(10, 20)
Point(15, 15)

从函数返回结构体

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

// 计算面积的结构体
struct Area {
int value;
};

// 函数返回结构体
Area calculateArea(Rectangle rect) {
Area area;
area.value = rect.width * rect.height;
return area;
}

int main() {
Rectangle rect = {5, 10};
Area rectArea = calculateArea(rect);
std::cout << "矩形面积: " << rectArea.value << std::endl;
return 0;
}

输出:

1
矩形面积: 50

6. 示例项目

示例项目:简单学生信息管理系统

这个项目将结合前面的知识点,创建一个简单的学生信息管理系统,允许添加、显示和查找学生信息。

需求分析

  • 添加新学生的信息(ID、姓名、成绩)
  • 显示所有学生的信息
  • 根据学生ID查找学生信息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <vector>
#include <string>

// 定义学生结构体
struct Student {
int id;
std::string name;
float grade;

// 成员函数打印学生信息
void printInfo() const {
std::cout << "学生ID: " << id
<< ", 姓名: " << name
<< ", 成绩: " << grade << std::endl;
}
};

// 添加学生
void addStudent(std::vector<Student>& students, int id, const std::string& name, float grade) {
Student newStudent = {id, name, grade};
students.push_back(newStudent);
std::cout << "添加学生成功。" << std::endl;
}

// 显示所有学生
void displayStudents(const std::vector<Student>& students) {
if (students.empty()) {
std::cout << "没有学生信息。" << std::endl;
return;
}
std::cout << "所有学生信息:" << std::endl;
for (const auto& student : students) {
student.printInfo();
}
}

// 根据ID查找学生
void findStudentById(const std::vector<Student>& students, int id) {
for (const auto& student : students) {
if (student.id == id) {
std::cout << "找到学生:" << std::endl;
student.printInfo();
return;
}
}
std::cout << "未找到ID为 " << id << " 的学生。" << std::endl;
}

int main() {
std::vector<Student> students;
int choice;
do {
std::cout << "\n===== 学生信息管理系统 =====" << std::endl;
std::cout << "1. 添加学生" << std::endl;
std::cout << "2. 显示所有学生" << std::endl;
std::cout << "3. 根据ID查找学生" << std::endl;
std::cout << "4. 退出" << std::endl;
std::cout << "请选择(1-4):";
std::cin >> choice;

if (choice == 1) {
int id;
std::string name;
float grade;
std::cout << "输入学生ID: ";
std::cin >> id;
std::cout << "输入学生姓名: ";
std::cin >> name;
std::cout << "输入学生成绩: ";
std::cin >> grade;
addStudent(students, id, name, grade);
}
else if (choice == 2) {
displayStudents(students);
}
else if (choice == 3) {
int searchId;
std::cout << "输入要查找的学生ID: ";
std::cin >> searchId;
findStudentById(students, searchId);
}
else if (choice == 4) {
std::cout << "退出系统。" << std::endl;
}
else {
std::cout << "无效选择,请重新输入。" << std::endl;
}
} while (choice != 4);

return 0;
}

代码解析

  1. 结构体定义:定义了一个 Student 结构体,包含 id、name 和 grade,并有一个成员函数 printInfo 来打印学生信息。
  2. 功能函数:
    • addStudent:向学生列表中添加一个新的学生。
    • displayStudents:显示所有学生的信息。
    • findStudentById:根据学生ID查找并显示学生信息。
  3. 用户交互:使用 do-while 循环和 switch-case 来处理用户的选择,实现添加、显示和查找学生信息的功能。

运行示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
===== 学生信息管理系统 =====
1. 添加学生
2. 显示所有学生
3. 根据ID查找学生
4. 退出
请选择(1-4):1
输入学生ID: 1001
输入学生姓名: Alice
输入学生成绩: 89.5
添加学生成功。

===== 学生信息管理系统 =====
1. 添加学生
2. 显示所有学生
3. 根据ID查找学生
4. 退出
请选择(1-4):1
输入学生ID: 1002
输入学生姓名: Bob
输入学生成绩: 92.0
添加学生成功。

===== 学生信息管理系统 =====
1. 添加学生
2. 显示所有学生
3. 根据ID查找学生
4. 退出
请选择(1-4):2
所有学生信息:
学生ID: 1001, 姓名: Alice, 成绩: 89.5
学生ID: 1002, 姓名: Bob, 成绩: 92

===== 学生信息管理系统 =====
1. 添加学生
2. 显示所有学生
3. 根据ID查找学生
4. 退出
请选择(1-4):3
输入要查找的学生ID: 1001
找到学生:
学生ID: 1001, 姓名: Alice, 成绩: 89.5

===== 学生信息管理系统 =====
1. 添加学生
2. 显示所有学生
3. 根据ID查找学生
4. 退出
请选择(1-4):4
退出系统。

零基础C++(8) 处理类型

Posted on 2024-09-28 | In 零基础C++

类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。

有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:

1
2
3
4
//wages 是double的同义词
typedef double wages;
//base是double的同义词,p是double*的同义词
typedef wages base, *p;

C++11

新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:

1
2
//64位整型
using int64_t = long long;

这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。

1
2
//定义变量a为64位整型
int64_t a = 10;

指针、常量和类型别名

如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。

例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:

1
2
3
typedef char * pstring;
const pstring cstr = 0;
const pstring *ps;

上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。

pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。

auto类型说明符

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。

然而要做到这一点并非那么容易,有时甚至根本做不到。

为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。

和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:

1
2
3
4
//计算求和
int age1 = 20;
int age2 = 35;
auto age_add = age1+age2;

auto很有作用,后期我们会学习尾置类型推导,以后再讲。

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

1
2
3
4
//正确, i是整数,p是整型指针
auto i= 0, * p= &i;
//错误, sz是整型,pi是double
//auto sz = 0, pi = 3.14;

复合类型、常量和auto

编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。

首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。

此时编译器以引用对象的类型作为auto的类型:

1
2
3
int i = 0, &r = i;
// a是一个整数,类型是r所引用的类型
auto a = r;

auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
int i = 0, &r = i;
// a是一个整数,类型是r所引用的类型
auto a = r;
// cr是一个常量引用,ci是int类型的常量
const int ci = i, &cr = ci;
// b是一个整数,ci顶层const被忽略了
auto b = ci;
// c是一个整数,cr是ci的别名,ci本身是一个顶层const
auto c = cr;
// d 是一个整型指针,i是整型
auto d = &i;
// e是一个指向整数常量的指针,对常量对象取地址是一种底层const
auto e = &ci;
}

如果希望推断出的auto类型是一个顶层const,需要明确指出:

1
2
//顶层const可显示指定,f是一个const int类型
const auto f = ci;

还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:

1
2
3
4
5
6
// g是一个整型常量引用,绑定到ci
auto &g = ci;
//错误,非常量引用不能绑定字面量
//auto &h = 42;
//正确,常量引用可以绑定字面量
const auto &j = 42;

要在一条语句中定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:

1
2
3
4
5
6
7
//i为int类型, ci为const int类型, 但是k是int类型,l是int&类型
auto k = ci, &l = i;
//m是对常量的引用,p是指向整数常量的指针
// p为const int*类型
auto &m = ci, *p = &ci;
//错误, i为int类型,&ci的类型为const int*
//auto &n = i, *p2 = &ci;

decltype类型指示符

C++11

有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。

为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

1
decltype(f()) sum = x; //sum的类型就是函数f的返回值的类型

编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。

decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

1
2
3
4
5
6
7
const int ci = 0, &cj = ci;
//x是const int类型
decltype(ci) x = 0;
//y是一个const int&类型,y绑定到x
decltype(cj) y = x;
//错误,z是一个引用,引用必须初始化
//decltype(cj) z;

因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。

需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。

decltype和引用

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。

有些表达式将向decltype返回一个引用类型。

一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

1
2
3
4
5
6
7
8
{
//decltype的结果可以是引用各类型
int i = 42, *p = &i, &r = i;
//正确,假发的结果是int,因此b是一个未初始化的int
decltype(r + 0) b;
//错误,c是int&,必须初始化
//decltype(*p) c;
}

因为r是一个引用,因此decltype(r)的结果是引用类型。

如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。

另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。

因此,decltype(*p)的结果类型就是int&,而非int。decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。

有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。

如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;

如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:

1
2
3
4
5
//decltype的表达式如果加上了括号的变量,结果就是引用
//错误,d是int&,必须初始化
//decltype((i)) d;
//正确,e是一个未被初始化的int类型值
decltype(r) e = i;

切记:decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

工作中的应用

工作中会利用auto和decltype配合使用,结合模板做类型推导返回动态类型,比如我们在并发编程系列课程中封装提交任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class F, class... Args>
auto commit(F&& f, Args&&... args) ->
std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {
using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));
if (stop_.load())
return std::future<RetType>{};
auto task = std::make_shared<std::packaged_task<RetType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<RetType> ret = task->get_future();
{
std::lock_guard<std::mutex> cv_mt(cv_mt_);
tasks_.emplace([task] { (*task)(); });
}
cv_lock_.notify_one();
return ret;
}

这段代码大家要学习模板,以及万能引用后才能完全吸收,我们留个伏笔,以后的剧情中会触发。

<1…567…41>

401 posts
18 categories
21 tags
RSS
GitHub ZhiHu
© 2026 恋恋风辰 本站总访问量次 | 本站访客数人
Powered by Hexo
|
Theme — NexT.Muse v5.1.3