前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++:数组与多维数组

C++:数组与多维数组

作者头像
用户7886150
修改2021-02-03 10:40:51
2K0
修改2021-02-03 10:40:51
举报
文章被收录于专栏:bit哲学院

参考链接: C++多维数组

一、什么是数组 

数组与vector类似,可以储存固定大小、类型相同的顺序集合,但是在性能和灵活性的权衡上与vector不同。并且元素应为对象,所以不存在引用的数组,但是存在数组的引用。与vector不同的是,数组的大小确定不变,不能随意向数组增加元素。如果不清楚元素的确切个数,请使用vector。定义数组的时候必须指定数组的类型,不允许使用 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];

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

引用必须被初始化,但是引用的本意是不含内存空间的,如果强行说占空间,也只是占的指针指向的对象的空间,只能说他不占新的空间。而且引用数组是直接拿另外一个数组初始化引用,但是我们知道数组不具备拷贝的功能,所以引用的数组不能初始化。引用的数组完全可以用指针数组实现,所以引用的数组完全没有出现的意义

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)指针也是迭代器 

vector和string的迭代器支持的运算,数组的指针全部支持。使用指针也可以遍历整个数组。直接指向数组对象名则是指向第一位类似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++当中并没有多维数组,多维数组其实就是数组的数组。 

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

一个维度表示数组本身大小另一个维度表示其元素大小

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;

}

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档