恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

零基础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;
}

这段代码大家要学习模板,以及万能引用后才能完全吸收,我们留个伏笔,以后的剧情中会触发。

零基础C++(7) 引用类型

Posted on 2024-09-21 | In 零基础C++

const限定符

1 const 的定义与作用

const 是 C++ 关键字,用于指示变量的值不可修改。通过使用 const,可以提高代码的安全性与可读性,防止无意中修改变量的值。

2 const 在变量声明中的位置

const 关键字通常放在变量类型之前,例如:

1
const int a = 10;

也可以放在类型之后,但这种用法较少见:

1
int const a = 10;

可以用一个变量初始化常量, 也可以将一个常量赋值给一个变量

1
2
3
4
5
//可以用一个变量初始化常量
int i1 = 10;
const int i2 = i1;
//也可以将一个常量赋值给一个变量
int i3 = i2;

const变量必须初始化

1
2
//错误用法,const变量必须初始化
//const int i4;

3 编译器如何处理 const 修饰的变量

const 修饰的变量在编译时会被视为只读,尝试修改其值会导致编译错误。此外,编译器可能会对 const 变量进行优化,如将其存储在只读内存区域。

注意

默认状态下,const对象仅在文件内有效

当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:

1
const int bufSize = 512;

编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。

为了执行上述替换,编译器必须知道变量的初始值。

如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义.

为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

我们创建一个global.h文件和global.cpp文件, 我们知道头文件只做变量的声明,之前我们在头文件添加变量的定义会导致连接错误。

那如果我们添加const变量的定义

1
2
3
4
#ifndef DAY08_CONST_GLOBAL_H
#define DAY08_CONST_GLOBAL_H
const int bufSize = 100;
#endif //DAY08_CONST_GLOBAL_H

在main.cpp和global.cpp中包含global.h,发现可以编译通过,虽然main.cpp和global.cpp中包含了同名的bufSize,但却是不同的变量,运行程序可以编译通过。

有时候我们不想定义不同的const变量,可以在global.h中用extern声明bufSize

1
extern const int bufSize2;

在global.cpp中定义

1
const int bufSize2 = 10;

同样可以编译通过。

为了验证我们的说法,我们可以在global.h中声明一个函数,用来打印两个变量的地址

1
2
//打印bufSize地址和bufSize2地址
extern void PrintBufAddress();

在global.cpp中实现PrintBufAddress()

1
2
3
4
void PrintBufAddress(){
std::cout << "global.cpp buf address: " << &bufSize << std::endl;
std::cout << "global.cpp buf2 address: " << &bufSize2 << std::endl;
}

然后我们在main.cpp中调用PrintBufAddress()函数,并且在main.cpp中打印两个变量地址

1
2
3
4
5
PrintBufAddress();
//输出bufSize地址
std::cout << "main.cpp buf address is " << &bufSize << std::endl;
//输出bufSize2地址
std::cout << "main.cpp buf2 address is " << &bufSize2 << std::endl;

程序输出

1
2
3
4
global.cpp buf address: 0x7ff67a984040
global.cpp buf2 address: 0x7ff67a984044
main.cpp buf address is 0x7ff67a984000
main.cpp buf2 address is 0x7ff67a984044

可以看出global.cpp中的bufSize和main.cpp中的bufSize不是同一个变量

技巧

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

const的引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

1
2
3
4
//定义常量
const int ci = 1024;
//用常量引用绑定常量
const int &r1 = ci;

不能修改常量引用的值

1
2
//不能修改常量引用的值
//r1 = 2048;

也不能用非常量引用指向一个常量对象

1
2
//也不能用非常量引用指向一个常量对象
//int& r2 = ci;

术语

常量引用是对const的引用

企业中,C++程序员们经常把词组“对const的引用”简称为“常量引用

允许将const引用绑定一个非const变量

1
2
3
int i5 = 1024;
//允许将const int& 绑定到一个普通的int对象上
const int &r5 = i5;

常量引用绑定字面量

1
2
//常量引用绑定字面量
const int &r6 = 1024;

常量引用绑定表达式计算的值

1
2
3
//常量引用绑定表达式计算的值
const int &r7 = r6 * 2;
const int &r8 = i5 * 2 + 10;

思考1

下面的代码能编译通过吗?

1
2
double dval = 3.14;
int & rd = dval;

答案

1
2
3
//错误用法,类型不匹配
double dval = 3.14;
int & rd = dval;

思考2

下面的代码能编译通过吗?

1
2
double dval = 3.14;
const int & ri = dval;

答案

1
2
3
//编译通过
double dval = 3.14;
const int & ri = dval;

上面的代码相当于

1
2
3
//上面代码会做隐士转换,相当于下面代码
const int temp = dval;
const int &rt = temp;

在这种情况下,ri绑定了一个临时量(temporary)对象。

所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。

C++程序员们常常把临时量对象简称为临时量。

对const的引用可能引用一个并非const的对象必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:

1
2
3
4
5
6
7
int i9 = 1024;
//非常量引用绑定i9
int &r9 = i9;
//常量引用绑定一个变量
const int &r10 = i9;
//可以同过非常量引用修改i9的值
r9 = 2048;

指针和const

指向常量的指针(pointer to const)

可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。

要想存放常量对象的地址,只能使用指向常量的指针:

1
2
3
4
5
6
7
8
//PI 是一个常量,它的值不能改变
const double PI = 3.14;
//错误,ptr是一个普通指针
//double * ptr = &PI;
//正确,cptr可以指向一个双精度常量
const double *cptr = &PI;
//错误,不能给*ptr赋值
//*cptr = 3.14;

指针的类型必须与其所指对象的类型一致,但是允许令一个指向常量的指针指向一个非常量对象

1
2
3
4
//可以用指向常量的指针指向一个非常量
int i10 = 2048;
//ptr指向i10
int *cptr2 = &i10;

const指针

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。

常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。

把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:

1
2
3
4
5
6
int errNumb = 0;
//curErr是一个常量指针,指向errNumb
int * const curErr = &errNumb;
const double pi2 = 3.14;
//pip 是一个指向常量对象的常量指针
const double *const pip = &pi2;

指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型

1
2
3
4
5
6
//错误,pip是一个指向常量的指针
//*pip = 2.72;
//可以修改常量指针指向的内容
*curErr = 1024;
//可以修改常量指针指向的地址
//curErr = &i10;

顶层const

指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题

用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量。

顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。

底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显:

1
2
3
4
5
6
7
8
9
10
11
int i = 0;
//不能改变p1的值,这是一个顶层const
int * const pi = &i;
//不能改变ci的值,这是一个顶层const
const int ci = 42;
//允许改变p2的值,这是一个底层const
const int * p2 = &ci;
//靠右边的const是顶层const,靠左边的const是底层const
const int * const p3 = p2;
//用于声明引用的const都是底层const
const int &r = ci;

底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换

1
2
3
//指针赋值要注意关注底层const
//p2拥有底层const,p4无底层const,所以无法赋值
//int * p4 = p2;

constexpr和常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。

我们先在global.h中声明一个全局函数返回固定大小

1
extern int GetSize();

在global.cpp中实现

1
2
3
int GetSize(){
return 20;
}

然后我们用const定义一些常量表达式

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如

1
2
3
4
5
6
7
8
9
10
{
//max_files是一个常量表达式
const int max_files = 20;
//limit是一个常量表达式
const int limit = max_files + 10;
//staff_size不是常量表达式,无const声明
int staff_size = 20;
//sz不是常量表达式,运行时计算才得知
const int sz = GetSize();
}

尽管staff_size的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非const int,所以它不属于常量表达式。

另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。

当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。

C++11新标准

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

1
2
3
4
5
6
//20是一个常量表达式
constexpr int mf = 20;
//mf+1是一个常量表达式
constexpr int limit = mf + 10;
//错误,GetSize()不是一个常量表达式,需要运行才能返回
//constexpr int sz = GetSize();

尽管不能使用普通函数作为constexpr变量的初始值,新标准允许定义一种特殊的constexpr函数。

这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。

我们在global.h中定义一个constexpr函数

1
2
3
inline constexpr int GetSizeConst() {
return 1;
}

为了避免在多个源文件中包含同一个头文件而导致的多重定义错误,可以将 constexpr 函数声明为 inline。

inline 关键字允许在多个翻译单元中定义同一个函数,而不会引起链接错误。

接下来在定义一个constexpr变量就行了

1
constexpr int sz = GetSizeConst();

指针和constexpr

必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:

1
2
3
4
//p是一个指向整形常量的指针
const int * p = nullptr;
//q是一个指向整数的常量指针
constexpr int *q = nullptr;

一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。

定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针

global_i是一个全局变量

1
2
3
4
5
//constexpr指针只能绑定固定地址
//constexpr int *p = &mvalue;
constexpr int *p = nullptr;
//可以绑定全局变量,全局变量地址固定
constexpr int *cp = &global_i;

可以修改constexpr指向的内容

1
2
3
constexpr int *p = &global_i;
//修改p指向的内容数据
*p = 1024;

问题

global_i是一个全局变量,下面这个指针是什么类型?能否修改cp指向的数据的内容(*cp = 200)?

1
constexpr const int * cp = &global_i;

零基础C++(6) 指针类型

Posted on 2024-09-17 | In 零基础C++

指针基础

在C++中,指针是一种特殊的变量,它存储的是另一个变量的内存地址,而不是数据本身。通过使用指针,我们可以直接访问和操作内存中的数据。指针也叫做地址。

和引用的区别

指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。

其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。

其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

1
2
3
4
5
6
//声明一个指向整数的指针,可以不赋初值
int *ptr;
//ip1和ip2都是指向int类型对象的指针
int *ip1, *ip2;
//dp2是指向double类型对象的指针,dp是double型对象
double dp, *dp2;

这里,ptr 是一个指针变量,它可以存储一个整数的内存地址。

指针赋值

我们可以通过如下方式(&变量)获取一个变量的地址

1
变量地址 = &变量名

指针可以通过两种方式赋值:

  1. 直接赋值内存地址:这通常不是常规做法,因为直接操作内存地址可能会导致未定义行为。
  2. 赋值变量的地址:这是更常见的做法,使用取地址运算符(&)来获取变量的地址,并将其赋值给指针。
1
2
3
//定义ptr2并且赋值,指向var地址
int var = 10;
int *ptr2 = &var;

这里,ptr 被赋值为 var 的内存地址。

我们对上述代码进行图解:

https://cdn.llfc.club/fb06f3724d53b3d9f9157aa9e0062a9.png

ptr2指向var的地址,也就是ptr2存储的是var的地址0x2be3。因为ptr2也是对象,所以计算机也会为它开辟空间,ptr2自己的地址为0x3f2b.

所以指针的地址也可以用另一个指针变量存储

1
2
3
4
5
6
7
8
9
//定义ptr2并且赋值,指向var地址
int var = 10;
int *ptr2 = &var;
//ptr_address 存储ptr2的地址
int** ptr_address = &ptr2;
std::cout << "var 的地址为: " << &var << std::endl;
std::cout << "ptr2 存储的值为: " << ptr2 << std::endl;
std::cout << "ptr2 地址为: " << &ptr2 << std::endl;
std::cout << "ptr_address 的值为: " << ptr_address << std::endl;

上述代码打印结果为

1
2
3
4
var 的地址为: 0xad6a5ffc24
ptr2 存储的值为: 0xad6a5ffc24
ptr2 地址为: 0xad6a5ffc18
ptr_address 的值为: 0xad6a5ffc18

可以看到我们通过ptr_address 可以存储ptr2的地址, ptr_address存储的是指针的地址,所以它是一个指针的指针类型(int**),也叫做二级指针。

类型匹配

因为在声明语句中指针的类型实际上被用于指定它所指向的对象的类型,所以二者必须匹配。

如果指针指向了一个其他类型的对象,那么会产生错误。

1
2
3
4
5
6
7
8
9
double dval = 3.14;
//正确,初始值是double类型的对象的地址
double *pd = &dval;
//正确,初始值是指向double对象的指针
double *pd2 = pd;
//错误,pi的类型和pd类型不匹配
//int * pi = pd;
//错误,视图把double型对象的地址赋值给int型指针
//int * pi = &dval;

指针值

指针的值(即地址)应属下列4种状态之一:

1.指向一个对象。

2.指向紧邻对象所占空间的下一个位置。

3.空指针,意味着指针没有指向任何对象。

4.无效指针,也就是上述情况之外的其他值。

试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。

访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。

尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象,所以试图访问此类指针(假定的)对象的行为不被允许。如果这样做了,后果也无法预计。

利用指针访问对象

我们可以利用*(解引用操作符)获取指针所指向的对象的数据,写法如下

1
数据变量 = *指针

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

我们看下这个例子

1
2
3
4
5
6
//访问对象
int ival = 42;
//p_int存放着ival的地址,或者说p_int是指向变量ival的指针
int * p_int = &ival;
//由符号*得到指针p所指向的对象,输出42
std::cout << *p_int ;

上面的例子通过*p_int获取p_int所指向的对象ival的值。输出42

利用指针修改对象

因为指针存储的是对象的地址,通过解引用获取对象的数据,我们也可以通过解引用修改对象的值,基本格式为

1
2
3
4
5
6
7
8
9
10
11
//访问对象
int ival = 42;
//p_int存放着ival的地址,或者说p_int是指向变量ival的指针
int * p_int = &ival;
//由符号*得到指针p所指向的对象,输出42
std::cout << *p_int << std::endl;
//由符号*得到指针p所指向的对象,即可经由p_int为变量ival赋值
*p_int = 0;
std::cout << * p_int << std::endl;
//打印ival的值
std::cout << "ival 的值为" << ival << std::endl;

注意

解引用操作仅适用于那些确实指向了某个对象的有效指针

1
2
3
4
//初始化一个空指针
int* empty_pointer = nullptr;
//打印空指针数据,非法
//std::cout << "空指针指向数据是 " << *empty_pointer << std::endl;

符号的多重含义

像&和*这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:

1
2
3
4
5
6
7
8
9
10
11
12
//符号的多重含义
int ival2 = 42;
//& 紧跟着类型名出现,因此是声明的一部分,r是一个引用
int &r = ival2;
//* 紧跟着类型名出现,因此是声明的一部分,p是一个指针
int *p;
// &出现在表达式中,是一个取地址符号
p = &ival2;
// * 出现在表达式中,是一个解引用符号
*p = ival2;
//& 是声明的一部分,*是一个解引用符号
int &r2 = *p;

空指针

空指针包含几种定义方式

1
2
3
4
5
6
7
8
9
10
11
//空指针定义方式
//C++11 最推荐方式
int *p1 = nullptr;
//直接将p2初始化为字面量0
int *p2 = 0;
//需要使用#include<cstdlib>
//等价于int * p3 = 0;
int *p3 = NULL;
std::cout << "p1: " << p1 << std::endl;
std::cout << "p2: " << p2 << std::endl;
std::cout << "p3: " << p3 << std::endl;

得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。

nullptr是一种特殊类型的字面值。

另一种办法就如对p2的定义一样,也可以通过将指针初始化为字面值0来生成空指针。

过去的程序还会用到一个名为NULL的预处理变量(preprocessor variable)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0。

注意事项

把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行

1
2
int zero = 0;
pi = zero;

地址变换

我们可以修改指针指向的地址,进而达到指向其他变量的目的.

指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。

一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。

指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象:

1
2
3
4
5
6
7
8
9
10
int ival3 = 42;
//pval3 被初始化,但没有指向任何对象
int *pval3 = 0;
//pval4被初始化,指向ival3的地址
int* pval4 = &ival3;
//将pval3的指向改为pval4的指向,二者同时指向ival3
pval3 = pval4;
std::cout <<"ival3 的地址为:" << &ival3 << std::endl;
std::cout <<"pval3 指向的地址为 " << pval3 << std::endl;
std::cout <<"pval4 指向的地址为 " << pval4 << std::endl;

运行上述程序可以看到输出

1
2
3
ival3 的地址为:0xf8329ff7a4
pval3 指向的地址为 0xf8329ff7a4
pval4 指向的地址为 0xf8329ff7a4

指针判空

有时候我们需要判断指针是否为空,可以通过if判断,if大家没学,此处仅作演示和理解。

在C++ 中0为false,非0为true,所以一个空指针通过if判断,是false, 非空指针为true

1
2
3
4
5
6
7
8
9
10
11
//空指针判断
int * empty_pointer2 = nullptr;
if(!empty_pointer){
std::cout << "empty_pointer is empty" << std::endl;
}

int test = 100;
int * normal_pointer2 = &test;
if(normal_pointer2){
std::cout << "normal pointer is not empty" << std::endl;
}

指针同样可以支持比较运算,判断相等(==),判断不等(!=)。

1
2
3
4
//判断指针是否相等
if(normal_pointer2 != empty_pointer){
std::cout << "normal_pointer 和 empty_pointer 不相等"<< std::endl;
}

万能指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

1
2
3
4
5
6
//万能指针
double obj = 3.14, *obj_pd = &obj;
//void 可以存放任何类型的对象的地址
void * pv = &obj;
//pv 可以存储任意类型的地址
pv = obj_pd;

利用void*指针能做的事儿比较有限:

拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。

不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

指向指针的指针

以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,
// 因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。
// 也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:
int ival4= 1024;
// pival4指向了int型的数
int *pival4 = &ival4;
//ppi指向了一个int型的指针
int ** ppi = &pival4;

std::cout << "The value of ival\n"
<< "direct value: " << ival4 << "\n"
<< "indirect value: " << *pival4 << "\n"
<< "doubly indirect value : " << **ppi
<< std::endl;

该程序使用三种不同的方式输出了变量ival的值:第一种直接输出;第二种通过int型指针pi输出;第三种两次解引用ppi,取得ival的值。

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

指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:

1
2
3
4
5
6
7
8
9
10
11
//引用本身不是一个对象,因此不能定义指向引用的指针。
// 但指针是对象,所以存在对指针的引用
int init = 42;
// pinit是一个int型的指针
int *pinit;
// rpinit是一个对指针pinit的引用
int *& rpinit = pinit;
// rpinit引用了一个指针,因此给rpinit赋值&init就是令pinit指向init
rpinit = &init;
//解引用rpinit得到i,也就是p指向的对象,将init改为0
*rpinit = 0;

指针运算

指针可以进行算术运算,如递增(++)和递减(–),这些操作会改变指针所指向的内存地址。但是,这种操作仅限于指向数组元素的指针。

指针和数组

在C++中,数组名在表达式中通常被当作指向数组首元素的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
//指针可以进行算术运算,如递增(++)和递减(--),
// 这些操作会改变指针所指向的内存地址。但是,这种操作仅限于指向数组元素的指针。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr_arr = arr;
std::cout << "ptr_arr is : " << ptr_arr << std::endl;
int firstElement = *ptr_arr;
std::cout << "firstElement is " << firstElement << std::endl;
// 递增指针
++ptr_arr; // ptr 现在指向 arr[1]
std::cout << "ptr_arr is : " << ptr_arr << std::endl;
// 访问新位置的值
int secondElement = *ptr_arr; // secondElement 等于 2
std::cout << "secondElement is " << secondElement;

上述程序输出

1
2
3
4
ptr_arr is : 0x3160fffa30
firstElement is 1
ptr_arr is : 0x3160fffa34
secondElement is 2

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

注意事项

  • 指针必须在使用前被初始化,否则它们可能包含垃圾值,导致未定义行为。
  • 指针运算(如递增和递减)仅适用于指向数组元素的指针。
  • 指针的解引用操作必须确保指针不是空指针(nullptr),否则会导致运行时错误。
<1234…37>

370 posts
17 categories
21 tags
RSS
GitHub ZhiHu
© 2025 恋恋风辰 本站总访问量次 | 本站访客数人
Powered by Hexo
|
Theme — NexT.Muse v5.1.3