字符串、向量和数组(三)

1.数组

数组是一种类似于标准库类型vector一种数据结构,但是在性能和灵活度上和vector又有所差异。与vector不同的是,数组的大小固定,不能动态地往里面添加元素,在一些特殊的场合使用数组,性能会好一些,但是相应地灵活度会差一些。

1.1定义和初始化数组

数组是一种符合类型,形式为:a[d],a为数组名,d为维度,即数组中元素的个数,它也是数组类型的一部分,编译的时候维度应该是已知的,所以维度应该是一个常量表达式。下面是一些例子:

unsigned num1=3; //不是常量表达式
constexpr unsigned num2=12;//常量表达式
int arr[10]; //正确 含有10个整数的数组
int *parr[10]; //正确 含有10个整形指针的数组
string str[num1]; //错误,num1不是常量表达式
string strs[get_size()]; //如果get_size返回的值是常量时候正确

默认情况下,数组的元素会被默认初始化。定义数组的时候必须要指定数组的类型,不能使用auto去推测类型,同时和vector一样,里面的元素必须是对象,不存在引用的数组。

显示初始化数组元素

可以对数组的元素进行列表初始化,这个时候可以忽略数组的维度,编译器可以推测出数组的维度;如果指定了数组的维度,那么初始值的总量就不能大于维度,否则出错;如果初始值的数量小于维度,那么剩下的元素将被初始化成默认值。下面看一些例子:

const unsigned int size=3;
int arr1[size]={0,1,2};
int arr2[]={0,1,2} //arr2的维度为3
int arr3[5]={0,1,2}; //元素为0,1,2,0,0
string str1[3]={"hello","world"};//元素为:hello world ""
int arr4[2]={1,2,3};//初始值的数量大于维度,错误

字符数组的特殊性

字符数组有一种额外的初始方式,就是使用字符串字面值进行初始化。当使用这种方式的时候需要注意,在字面值的结尾处还有一个空白符,这个空字符也会被拷贝到字符数组中去。下面看一下举例:

char ch1[]={'h','e'}; //没有空字符
char ch2[]={'h','e','\0'}; //含有一个显示的空字符
char ch3[]="C++"; //自动添加表示字符串结束的空字符
char ch4[5]="apple"; //错误 没有空间存放空字符

不允许拷贝和赋值

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

int a[]={1,2,3};
int b[]=a; //错误,不能将数组的内容拷贝给其他数组作为其初始值
b=a; //错误,不能将数组赋值给其他数组

理解复杂的数组声明

和vector一样,数组能存放大多数的类型。外面可以定义存放指针的数组,因为数组也是对象,所以我们还可以定义数组的指针和数组的引用,后两种在定义上比较复杂。例子:

int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10]; //不存在引用的数组
int (*Parray)[10]=&arr; //数组的指针,Parray指向一个含有10个整数的 数组
int (&arrRef)[10]=arr; //数组的引用 arrRef引用一个含有10个整数的数组

默认情况下,类型修饰符从右往左依次绑定。如果有括号,就是按照由内到外的顺序,例如上述的Parray和arrRef。对于修饰符的 数量,并没有具体限制,看一个例子:

int *(&arr)[10]=ptrs;

按照由内而外的顺序阅读,首先arr是一个引用,然后往右看知道arr引用的对象是一个大小为10的数组,最后观察左边发现 ,数组的元素类型是指向int的指针。分析得到,arr是一个含有10个int型指针的数组的引用。

2.访问数组元素

数组的元素可以使用范围for语句或者是下标运算符来访问,数组元素的索引是从0开始的。在使用数组下标的时候,通常将其的类型定义为size_t。size_t在头文件cstddef定义,是一种机器相关的 无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。除了大小固定这一特点外,其他用法与vector对象相似。下面是一个使用数组统计各个分数段成绩的例子:

unsigned score[11]={};
unsigned grade;
while(cin>>grade)
{
    ++score[grade/10]; //以每10分为一段
}

与vector和string对象一样,数组可以使用范围for语句遍历数组中所有的元素,下面使用范围for遍历输出数组score中所有的元素:

for(auto s:score)
{
   cout<<s<<" ";
}
cout<<endl;

因为维度是数组的一部分,编译器知道数组中有多少元素,所以使用范围for可以减少人为控制遍历过程的负担。

检查下标的值

和string和vector对象一样,数组需要检测下标是否合法,下标必须大于等于0小于数组长度。如果数组越界,它在编译的时候可能顺利执行通过,所以需要谨慎处理细节才能避免一些致命的错误。常见的越界访问非法内存带来的问题就是缓冲区溢出错误。

1.3指针和数组

C++语言中,数组和指针有着紧密的联系。使用数组的时候,编译器一般会将数组转化为指针。取地址符适用于任何对象,同样也适合数组,可以使用取地址符得到指向该元素的指针,例如:

string num[]={"hello","world","haha"};
string *p=&num[0]; //指针p指向数组num的第一个元素

数组还有一个特性,就是在很多使用到数组的地方,编译器会自动将其转化为指向数组第一个元素的指针,例如:

string num[]={"hello","world","haha"};
string *p=num; //指针p指向num的第一个元素

由此 可以感知,有些情况下,对数组的操作其实就是对指针的操作。这里面有很深的意思,其中一层的意思是使用数组作为auto变量的一个初始值时候,推断出来的类型为指针而不是数组。例如:

int a[]={1,2,3};
auto b(a); //b的类型是int* 等价于auto b(&a[0]);

需要注意的是,如果使用decltype关键字时,上述的转化将不会发生。看下面的例子:

int a[10]={};//a有10个元素,初始值为0
decltype(a) b; //decltype(a)的返回值是有10个元素的数组
b=p; //不能将指针赋值给数组
a[5]=5; //正确,将5赋值给数组中的第六个元素

指针也是迭代器

指向数组元素的指针具有更多功能,string和vector的迭代器所支持的运算,数组的指针全部支持。例如可以使用递增运算符是指向数组元素的指针向前移动一个位置:

int ptr[10]={1,2,3,4,5};
int *p=ptr; //指针p指向第一个元素
p++; //指针p指向ptr[1]

使用迭代器可以遍历vector对象中的元素,同样使用指针也可以遍历数组中的元素,前提是获得指向数组首元素的指针和指向数组尾元素下一个位置的指针。首指针通过数组名或者是或者数组中首元素的地址获得,尾元素则要使用到数组的一个特殊性质:外面可是设法获取尾元素后面那个并不 存在的元素的地址。例如:

int a[10]={};
int *end=&a[10]; //end指向尾元素的下一位置,不指向具体的元素

尾后后指针不指向具体的元素,因此不允许尾指针解引用和递增操作。下面是一个使用指针遍历数组所有元素的例子:

int a[10]={};
int *end=&a[10];

for(int *start=a;start!=e;++start)
{
   cout<<*start<<" ";
}
cout<<endl;

标准库函数begin和end

为了解决上述寻找尾后指针容易出错的情况,C++11提供了标准库函数begin和end用于获取首指针和尾后指针,函数传入的参数为数组。下面是一个 用指针遍历数组,寻找出第一个负数的例子;

int a[10]={1,2,3,4,5,-9,10,89,76,80};
int *begin=begin(a);
int *end=end(a);
while(begin!=end && *begin>=0) //遇到负数,退出循环
{
    ++begin;
}

指针运算

指向数组元素的指针可以执行所有迭代器的运算,这些运算包括解引用、递增、与整数相加、两个指针相加减等,用在指针和迭代器上的意义完全一样。
从一个指针加上(减去)一个整数值,结果仍然是指针。新指针指向的元素与原来的指针相比,向前移动了该整数个位置。

constexpr size_t sz=5;
int arr[5]={1,2,3,4,5};
int *p=arr; //p指向首元素
int *p2=p+4; //p4指向arr[4]

给指针加上一个整数,要么是指向同一数组的其他元素,要么指向尾元素的下一位置。例如:

int *p=arr+sz; //p指向尾元素的下一位置
int *p2=arr+10; //错误,通过计算所得 的指针超出了上述范围就将产生错误,但更为严重的是,编译器发现不了这个错误

两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,ptrdiff_t也是一种定义在cstddef头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t是一种带符号类型。
只要两个指针指向的是同一个数组的元素,或者是指向尾元素的下一位置,那么就能使用关系运算符对它们进行比较;如果两个指针指向不同的对象,则它们不能进行比较。上述的指针运算同样适用于空指针和所指对象并非是数组的指针。在后一种情况中,两个指针所指的必须是同一个对象或者对象的下一位置。如果是空指针,允许加上或者减去一个值为0的常量表达式,两个空指针允许相减,结果是0。

解引用和指针运算的交互

如果指针指向数组中的一个元素,就可以解引用该指针,得到所指向的值。如果表达式中有解引用运算符,那么就需要注意括号是否需要,例如:

int a[5]={1,2,3,4,5};
int last=*(a+4); //last为a[5]的值
int last=*a+4; //last的值为a[0]+4,注意和上面的区别

下标和指针

对数组执行下标运算实际上就是对数组元素的指针执行下标运算,只要指针指向的是数组中的元素或者尾元素的下一位置,都可以进行下标运算,例如:

int *p=&a[2]; //p指向a[2];
int num=p[1]; //p[1]等价于*(p+1),即为a[3]
int k=p[-2]; //p[-2]等价于*(p-2),即为a[0]

在这边我们可以看出,数组的下标可以是负数,而string和vector虽然也能执行下标运算,但是下标必须是无符号型数。

4.C风格字符串

C标准库String函数

它们用于操作C语言风格的字符串,放在cstring头文件中。

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

char a[]={'a','b','c'};
cout<<strlen(a)<<endl; //错误,a并没有以空字符结尾
char b[]={'a','b','c','\0'};
cout<<strlen(b)<<endl; //正确,b是以空字符为结尾

比较字符串

比较标准库string对象的 会后,用的是普通的关系运算符和相等性运算符;如果使用这些运算符用在两个C风格字符串上,实际上比较的将是指针而非字符串本身。例如:

string s1="A String haha";
string s2="A String game";
if(s1>s2) //true:s1大于s2

const char ca1[]="A String a";
const char ca2[]="A String b";
if(ca1<ca2) //两个都是const char*类型 未定义的:试图比较两个无关地址

对于C语言风格的字符串比较,需要使用strcmp函数,此时比较的不再是指针。如果两个字符串相等,返回0,;如果前一个字符串的字面值较大,返回一个正数;如果第二个字符串的字面值较大,则返回一个负数。例如:

if(strcmp(ca1,ca2)) 

目标字符串的大小由调用者决定

连接和拷贝C语言风格的字符应该使用strcat和strcpy函数。要使用这两个函数,还需要提供一个存放字符串的数组,必须保存该数组的大小足够放下结果字符串和空字符,这种操作充满风险,极易发生错误。例如:

//在 这边必须保证largeStr的大小足够大
strcpy(largeStr,cal); //将cal拷贝给largeStr
strcat(largeStr," "); //在largeStr的尾部加上一个空格
strcat(largeStr,ca2); //把ca2连接到largeStr的后面

1.5与旧代码的接口

C++提供了一组功能简化这工作。

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

我们经常会使用字符串字面值初始化string对象,更一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代。
如果程序需要使用C语言风格字符串,无法使用string对象来替代它。我们 不能用string对象初始化一个指向C语言风格字符串的指针,但是我们可以使用string类的c_str成员函数来返回一个指向一个以空字符为结尾的字符数组,这个数组所存的数据和string对象的一样。例如:

string s("hello");
const char *pstr=s.c_str(); //指针的类型是const char*,数组中的所有元素不能内修改

使用数组初始化vector对象

C++规定不允许使用数组给内置类型的数组赋值,也不允许使用vector对象给数组赋初值,但是允许数组给vector对象赋初值。要实现这个目的,我们只需提供数组的首地址和尾后地址就可以了,例如:

int a[]={1,2,3,4,5};
vector<int> ivec(begin(a),end(a)); //ivec将包含6个元素,顺序和值 都和数组一样

用于初始化vector对象的值也可以仅仅是数组的一部分,例如:

vector<int> ivec(a+1,a+4); //ivec含有3个元素,分别为a[1] a[2] a[3]

现代C++程序要求尽量使用标准库类型而不是数组。

2.多维数组

严格意义上讲,C++没有定义多维数组,通常的多维数组其实是数组的数组。对于二维数组而言,通常把第一个维度 称为行,第二个维度称为列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值