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};
//等价于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 << " ";
}

文章作者: 山木
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 山木 !
  目录