C++:数组与多维数组

一、什么是数组

  1. 数组与vector类似,可以储存固定大小、类型相同的顺序集合,但是在性能和灵活性的权衡上与vector不同。并且元素应为对象,所以不存在引用的数组,但是存在数组的引用。
  2. 与vector不同的是,数组的大小确定不变,不能随意向数组增加元素。
  3. 如果不清楚元素的确切个数,请使用vector。
  4. 定义数组的时候必须指定数组的类型,不允许使用 auto 关键字由初始值的列表推断类型。

 

二、定义和初始化内置数组

  • 数组的声明形如a[ b ],其中a是数组的名字,b是数组的维度。
  • 维度必须大于0,且维度是一个常量表达式,这也符合数组的大小确定不变的要求。
unsigned cnt = 42;                //不是常量表达式
constexpr unsigned sz = 42;       //常量表达式constexpr声明,能够让编译器判断是否为常量表达式,且得出表达式的结果
int arr[10];                      //含有10个整数的数组
int *parr[sz];                    //含有42个整数指针的数组
string bad[cnt];                  //错误,cnt不是常量表达式
string strs[get_size()];          //当get_size()是constexpr时正确;否则错误
//默认初始化会让数组含有未定义的值
constexpr int a = 10;
	int b[a];
	for (int i = 0; i < a; i++)
		cout << b[i] << endl;

运行结果:

 

(1)显式初始化数组元素

  • 可以对数组的元素进行列表初始化,此时允许忽略数组的维度。
  • 如果声明时没有指明维度,编译器会根据初始值的数量计算并推测出来。
  • 如果指明了维度,那么初始值的总数量不应该超出指定的大小。
  • 如果维度比提供的初始值大,那么初始化初始值后,剩下没初始值的维度元素被初始化为默认值
const unsigned sz = 3;
int ia[sz] = {0, 1, 2};            //含有3个元素的数组,元素值分别是0,1,2
int a2[] = {0, 1, 2};              //维度是3的数组
int a3[5] = {0, 1, 2};             //等价于a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"};      //等价于a4[] = {"hi", "bye", " "}
inr a5[2] = {0, 1, 2};             //错误,初始值过多

 

(2)字符数组的特殊性

与介绍string一样,将char数组拷贝给string时,必须将' \0 '作为结尾。

在进行列表初始化时,必须以' \0 '结尾,或者直接用" "自动添加表示初始化

 

C标准库中的字符串处理程序,是只认'\0'的,只要没找到'\0',它就认为字符串没有结束,拼命地往后找,这个寻找的过程不理会可能已经超过书柜的格数了(计算机其实很蠢);同样,也可能你在一排书中的中间抽走一本,在那个位置上写上'\0',那么愚蠢的计算机也会认为书到这里为止,它不理会后面其实还有(这是某种截断字符串的技巧)。

 

char c1[] = { 'c','+','+' };                 //列表初始化,没有空字符,会出现多的内容
char c2[] = { 'c','+','+','\0' };            //列表初始化成功
char c3[] = "c";                             //自动添加' /0 '到尾部  
const char c4[6] = "abcdef";                 //错误,没有空余位置存放空字符
cout << c1 << endl;
cout << c2 << endl;
cout << c3 << endl;
	

将c4注释后的运行结果: 

 

错误提示: 

 

(3)不允许数组与数组之间的拷贝和赋值

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

int a[] = {0, 1, 2};        //含有3个整数的数组
int a2[] = a;               //错误,不允许用数组初始化另一个数组
a2 = a;                     //错误,不能把一个数组直接赋值给另一个数组

 

(4)理解复杂的数组声明

数组本身就是对象,所以允许定义数组的指针及数组的引用。

引用数组是不合法的,而且指针数组完全可以代替引用数组,编译器也不知道给引用的数组分配多少内存,所以这种做法是不存在的。

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

理解数组声明,应从内向外理解,比如int (*Parray)[10];,我们先看内,也就是括号内,是一个指针,然后看括号的右边,所以理解为是指向大小为10的数组的指针,再看括号左边,理解为数组中10个元素都为int类型。

接下来让我们理解一下,什么是引用的数组和数组的引用

//arrs首先向右结合,相当于(int&) arr[10],表示arr是一个数组,其中的元素是引用,称之为引用的数组
int &arr[10];

//arr首先和&结合,所以arr是引用,引用的对象是数组,称之为数组的引用
int (&arr)[10];

为什么引用的数组是不合法的呢?

  1. 引用必须被初始化,但是引用的本意是不含内存空间的,如果强行说占空间,也只是占的指针指向的对象的空间,只能说他不占新的空间。而且引用数组是直接拿另外一个数组初始化引用,但是我们知道数组不具备拷贝的功能,所以引用的数组不能初始化。
  2. 引用的数组完全可以用指针数组实现,所以引用的数组完全没有出现的意义
char c1[] = "C++";                //自动添加' \0 ',所以这个字符数组维度为4
char(*a)[4] = &c1;                //指针数组,指向c1
	cout << *a << endl;       //输出内容为:C++ 

     3. 编译器并不知道应该给引用的数组分配多大的内存

数组的引用:

char c1[] = "C++";
char(&a)[4] = c1;
	cout << a << endl;        //输出:C++

 引用的数组,数组的引用区别:

int &arr[] = arr1;          //(int&) arr[] = arr1,arr[]是数组,相当于arr1拷贝给arr

int (&arr[]) = arr2;        //&arr[] = arr2,相当于一种指向,相当于arr指向了arr2,成为别名

 

(5)访问数组元素

与vector和string一样,可以用for语句或下标运算符来访问。数组索引从0开始,包含10个元素的数组,他的索引从0到9。

例子:输入分数,输出分段计数,以10分为一个分段,0-9,10-19以此类推,输入非数字为结束符输出分段

    unsigned scores[11] = {};
	unsigned grade;
	while (cin >> grade)
	{
		if (grade <= 100)
			++scores[grade / 10];
	}
	for (auto i : scores)
		cout << i << "";
	cout << endl;

输出结果: 

 

三、指针和函数

  • 在C++中,使用数组时,编译器会把他转换成指针。
  • 使用取地址符来获取指向某个对象的指针。
  • 对数组使用取地址符,就能得到指向该元素的指针。
string nums[] = {"one", "two", "three"};        //数组的元素是string对象
string *p = &nums[0];         //p指向nums的第一个元素     
  • 当直接拿指向对象名是,编译器会默认将对象替换为一个指向数组首元素的指针。
string *p2 = nums;            //等价于&nums[0]     

因为数组在使用时会替换成指针,所以将数组auto给一个变量的初始值时,推断得到的类型是指针而非数组

int ia[] = {0,1,2,3,4,5,6,7,8,9};        //设置一个含有10个数的数组
auto ia2(ia);                //ia2是一个整型指针,指向ia的第一个元素,等价于int *ia2=(&ia[0])
ia2 = 42;                    //错误,ia2是一个指针,不能指向字面值    

 

如果想让编译器推断出数组,给变量赋予类型声明,则需要用到decltype(ia)函数,ia是想要编译器推断类型的对象。

    int ia1[] = { 0,1,2,3,4,5,6,7,8,9 };        //ia1是含有10个元素的数组
	decltype(ia1) ia2 = {1,2};                  //让编译器推断ia1类型
	for (auto i : ia2)
		cout << i << " ";
	cout << endl;

输出结果:很显然,decltype顺便把ai1的维度推断并赋予了ia2

 

(1)指针也是迭代器

  1. vector和string的迭代器支持的运算,数组的指针全部支持。
  2. 使用指针也可以遍历整个数组。
  3. 直接指向数组对象名则是指向第一位类似begin()函数,如果指向尾元素后的一个不存在的元素,则与end()函数相似,但是这种方法容易出现错误。
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
inr *p = arr;        //p指向arr的第一个元素
++p;                 //P指向arr的第二个元素
int *e = arr[10];    //arr含10个元素,下标从0-9,如果指向10,则是第11个元素,是不存在的

//我们可以使用数组这种特殊的性质,遍历输出整个数组
//注意一点:当我们使用这种方法遍历,我们不能对尾后指针进行解引用,因为尾后指针并不指向具体元素,解引用会发生错误
for(int *p = arr; p != e; p++)
    cout<< *p <<endl;

 

(2)标准函数begin和end

与迭代器函数相似,但是数组始终不是类类型,所以不能用点操作符(.)使用函数,而是应该讲数组作为他们的参数。

注意:使用这种操作时,需要带上<iterator>头文件,当解引用和递增尾后元素的时候,编译器出错,与迭代器失效类似。

#include<iterator>
using std::begin;
using std::end;

int arr[] = {0, 1, 2, -1};
int *beg = begin(arr);        //beg是指向首元素的指针
int *last = end(arr);         //last是指向尾元素的下一个元素的指针(简称尾后元素)

//例子:遍历寻找函数中第一个负数
while(beg != last && *beg >= 0)    //如果使用beg不为尾后元素的指针与上beg解引用得到的值大于等于0则继续遍历
    ++beg;

//如果beg已经是尾元素的下一个元素,则跳出循环
//如果beg解引用后的值为负,则跳出循环


(3)数组指针运算

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

当数组指针加或者减去一个整数时,指针的指向会向前或向后移动一个整数位置,得到的结果仍是一个指针。

constexpr size_t sz = 5;       //使用size_t类型需要带上<cstddef>头文件,增强了可移植性,相当于unsigned int,其大小足以保证存储内存中对象的大小
int arr[sz] = {1, 2, 3, 4, 5};
int *p1 = arr;                 //p1指向arr[0]
int *p2 = p1 + 4;              //相当于让p1移动4位,p2指向arr[4]

int *p = arr + sz;            //相当于arr+5,让p指向arr[6],运行正常,但是值是内存中存放的未知数值,编译器不会发现错误
int *p2 = arr + 10;           //超出范围,直接显示目标内存中存放的数值

//和迭代器一样,如果让两个指针相减,结果是他们之间的距离。参与运算的两个指针必须是指向同一个数组当中的元素
auto n = end(arr) - begin(arr);        //n的值为5,是arr中的元素数量

//两指针相减,结果的类型是ptrdiff_t的标准库类型,和size_t一样,他也是定义在cstddef头文件的机器相关的类型,因为相减可能为负数,所以他是个signed类型

//注意:使用end参数时需要带上iterator头文件

由上面的代码可知,我们还可以使用另外一种方法指向尾后元素:arr + sz

int *a = arr, *e = arr + sz;        //遍历arr所有元素,此例子没有意义,但是能说明另一种遍历方法
while(b<e)                          //前提是指向的内容都为同一数组内的元素才可以这样做
    ++b;

 

(4)解引用和指针运算的交互

int ia[] = {0, 1, 2, 3, 6};
int last = *(ia + 4);        //正确:把last初始化为6,指针加上个整数表示向前移动4位,而对象名默认下标为0,所以是ia第4个下标的数值   
                             //等价于ia[4]

int last = *ia + 4;            //last初始化为4,由于优先级,先解引用ia后得到的0与4相加

运算符优先级表在《C++ Primer》第147页。

 

(5)下标和指针

多数情况下使用数组的名字其实用的是一个指向数组首元素的指针。

string和vector也可以使用下标,但是他们的下标必须是无符号类型。而数组允许处理负值这也是与string和vector的区别,但必须指向原来的指针所指的同一数组中的元素或尾后元素。

int ia[] = {0, 1, 2, 3, 4};
int i = ia[2];       //ia先转换为ia[0],再(ia[0] + 2)得到ia[2]
int *p = ia;         //p指向ia的首元素
i = *(p + 2)         //等价于i = ia[2];

int *p = &ia[2];    //p指向索引为2的元素
int j = p[1];       //p[1]等价于*(p + 1),也就是元素ia[3]
int k = p[-2];      //p[-2]等价于*(p - 2),相当于ia[0]

 

四、C风格字符串(char[])

  • C++支持C风格字符串,但是C风格字符串使用起来不方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
  • 字符串面值的结构就是C++由C继承而来的C风格字符串。
  • C风格字符串不是类型,而是约定俗成的表达和使用字符串的写法。
  • 按照此习惯必须在字符串中以空字符串' \0 '结束。

 

(1)C标准库string函数

下面列举了C语言标准库提供的一组函数,他们呗定义在cstring头文件中。

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

上面所列举的函数,不负责验证其字符串参数

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

char ca[] = {'C', '+', '+'};        //不以空字符结束
cout << strlen(ca) << endl;         //输出长度会长于ca,输出15

strlen可能会沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。

 

(2)比较字符串

当使用string进行比较时,用的是普通的关系运算符和相等性运算符:

string s1 = "ONE";
string s2 = "two";
if(s1<s2)            //true,s1<s2

当使用C风格字符串进行比较是,实际比较的是指针而非字符串本身,在数组的知识当中,我们知道了直接使用数组名,编译器则会将数组直接转换为指向第一个数组对象的指针。

const char ca1[] = "one";
const char ca2[] = "two";
if(ca1 < ca2)                //未定义,试图比较两个无关的地址

//根据上面的知识我们知道,指针数组的元素比较,需要是指向同一个数组的元素才能进行比较

 

如果想要比较两个C风格字符串需要用strcmp函数,这时候就不是进行指针比较了,而是字符串与字符串本身的对比。

const char ca1[] = "one";
const char ca2[] = "two";
if(strcmp(ca1. ca2)<0)      //与s1<s2含义相同,字符串本身的对比

//如果ca1 = ca2返回0,ca1 > ca2返回正值,ca1 < ca2返回负值

 

五、与旧代码的接口

如果我们新写成的代码,想要跟没有string与vector时代的代码相关联,为了衔接这一操作,C++提供了一些功能。比如旧程序的某处需要使用一个C风格字符串,但编译器无法直接用string对象来替换他,我们就可以使用c_str()函数返回一个C风格字符串。

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

为了让旧程序与string衔接:

string s("string");
char *sr = s;       //错误,不能用string初始化char *
const char *str = s.cstr();    //正确,cstr将s转换成了const char*

//当我们改变了s值,上述的指针则会失效,这时候我们需要重新cstr赋值一遍
string s = s + "s";
const char *str = s.cstr();

 

(2)使用数组初始化vector对象

  • 我们不可以拿一个数组为另一个内置类型(最原始的数组char [])的数组赋初值
  • 也不运行使用vector来初始化数组对象。
  • 但是允许数组初始化vector对象
  • 总之,数组的领地我们不能触犯,但是允许数组触犯其他类型的领地

 

vector可以拷贝数组,只要明确拷贝区域的首元素尾后地址就可以了

int ia[] = {0, 1, 2, 3, 4, 5}
vector<int> ivec(begin(ia), end(ia));        //跟之前指针指向数组首、尾后地址一样,将begin和end用做参数即可

//如果想拷贝2-4下标范围内的元素给vector对象
vector<int> ivec(ia + 2, ia + 4);            //数组对象指向下标0的位置,直接递增即可

 

六、多维数组

C++当中并没有多维数组,多维数组其实就是数组的数组。

当一个数组的元素仍是数组时,通常用两个维度来定义他:

  1. 一个维度表示数组本身大小
  2. 另一个维度表示其元素大小
int ia[3][4];    //数组总体积为3个元素,每个元素都是4个整数的数组

//对于数组的理解都是由内向外的,从定义的名字开始,
//ia是含有3个元素的数组,而这3个元素的数组中,
//每个元素都含有4个元素的数组,
//由左边我们知道,
//这些元素都是int类型


int arr[10][20][30] = 0;    //数组大小为10,10个元素大小都为20的数组,20个数组中每个数组都有30个整数元素

(1)多维数组的初始化

  • 允许使用嵌套式的列表初始化方法,也可以不用嵌套,直接一个列表初始化。
  • 可以只初始化每一个二维数组当中的第一个元素,但是这种情况必须用嵌套。
int ia[3][4] = {        //数组大小为3个元素,每个元素都是4个整数的数组
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};

int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};    //与上面的嵌套初始化等价

int ia[3][4] = {{0}, {1}, {2}};        //只初始化每行的首元素,其他元素为0

int ia[3][4] = {0, 1, 2, 3};           //如果没有嵌套,则只初始化第一行的4个元素,其他元素为0

 

(2)多维数组的下标引用

  • 可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。
  • 如果表达式中含有的下标运算符数量和数组的维度一样,那么表达式的结果是那个数组的原形。
  • 如果小于原始数组下标,则给的是索引出的一各内层数组。
int arr[10][20][30];
ia[3][4];        //三行四列
ia[2][3] = arr[0][0][0];       //下标从0开始,左值则为第三行的第四列元素
                               //利用arr的首元素给ia最后一行的最后一个元素赋值

int (&row)[4] = ia[1];         //先定义一个含有4个元素的数组的引用,将引用绑定到第二列四个元素上

用for语句处理多维数组:

constexpr size_t rowCnt = 3, colCnt = 4;
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;
	}
}
for(int i = 0;i < rowCnt;i++)
{
	for(int j = 0; j < colCnt;j++)
	cout << ia[i][j] << " " <<endl;
}

//输出0-11,总共12个整数

 

(3)使用范围for语句处理多维数组

在c++11新标准中新增了范围for语句,上面的for语句可以简化为下面的形式:

size_t cnt = 0;    
for(auto &row : ia)            //外层数组每一个元素(每一行)
    for(auto &col : row){      //内层数组每一个元素(每一列)
    col = cnt;                 
    ++cnl;
}

每次迭代都将cnt的值赋给ia的当前元素,然后将cnt+1。

这里将row和col定义为引用的原因是,如果不采用引用,则每个元素都会直接指向ia数组的首元素,这与我们需要遍历整个元素的目的区别太大。所以必须要把遍历的元素全部变为数组的引用才可以进行此项操作。

 

(4)指针和多维数组

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

int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
int (*p)[4] = ia;        //让p指向含有4个整数的数组
for(auto i:*p)
	cout << i << endl;    //输出内容为0,1,2,3

p = &ia[2];               //让p指向四个尾元素
    cout << i << endl;    //输出8,9,10,11


int (*p)[4] = ia;        //指向含有4个元素的数组,遍历输出0-3
int *p[4] = {*ia};          //整型指针的数组,遍历输出0-3
//上述内容主要看符号优先级,()优先级大于[]大于*

指针数组数组指针

int *p[n]     指针数组(p+1指向下一个):首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。

int (*p)[n]   数组指针(行指针,如果p+1,直接指向下一行):首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称

 

C++11新标准的提出,通过使用auto或者decltype就能尽可能避免使用指针数组和数组指针了。

for(auto p = ia;p != ia + 3;++p)
{
    for(auto q = *p;q != *p + 4;++q)
        cout << *q << '';
    cout << endl;
}

运行结果:

 

我们使用begin和end也可以实现以上功能,而且要更加简洁。

for (auto p = begin(ia); p != end(ia); ++p)
{
	for (auto q = begin(*p); q != end(*p); ++q)
		cout << *q << " ";
	cout << endl;
}

*p相当于ia[p][q],第一个for是从p[0]首元素开始,第二个for是从p[0][0]开始到p[0][3]结束一个循环,跳出再到第一个个for,p[1]开始,以此类推直到尾元素为止。

 

(5)类型别名简化多维数组的指针

这项操作能让我们更简便地去读写一个指向多维数组的指针。

using int_array = int[4];        //让int[4]的别名为int_array
typedef int int_array[4];        //与上面的作用相同

//循环输出ia
for(int_array *p = ia;p != ia + 3;++p)
{
    for(int_array *q = ia;q != ia + 4;++q)
        cout << *q << ' ';
    cout << endl;
}

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值