【计算机本科补全计划】《C++ Primer》:数组全解!!

正文之前

其实我的《C++ Primer》 已经看到第五章了,但是因为码字比较费时间,所以暂时没有迅速更新实在是对不住,但是没办法, 总不能一天拿出五六个小时来码字吧。最多三个小时不能多了。不过我后期会把码字当做是一种复习和笔记行为逐步跟上的。至少保证在我这儿可以完整的把《C++ Primer》从头到尾撸一遍。

正文

1、 数组的定义和初始化

数组是一种类似于标准库类型vector的数据结构,但是在性能与灵活性的权衡上又与vector不同,最大的不同是:数组的长度直接显式的或者间接地被规定了,是不变的。不能随意向数组中添加元素。因为这个特性,所以某些时候数组的性能较好,但是缺乏灵活性。

  • 数组的长度必须是给定的常量表达式,书上是这么说的,按照书上的说法下面应该报错,但是我的gcc给我的回复是没有错误。不过大家还是尽量按照《C++ Primer》的要求来。
int len=10;
int array[len]
  • 数组的初始化方式:
const unsigned sz=4; 
int ial[sz]={1,2,4};  {1,2,4,0}
int a2[]={1,24,3}; // {1,24,3}
itn a3[5]={0,1,2};   //{0,1,2,0,0}
string a3[3]={"abc","def"};   // 三个字符串 “abc”,“def”,“”
int a5[2]={1,2,3};      //错误:初始值是三个,但是容量为2;
  • 特殊的字符数组 其特殊之处在于可以用字符串字面值,来给字符数组赋值,但是一定要记住,赋值之后,会默认的多出一个\0结束符号,这个符号会被放到初始化的内容的最后一位,也就是说如果你输入了6个字符,那么编译器会默认在第七位加上\0符号作为arr[6]
char a1[]={'c','b','a'};  // {'c','b','a'}
char a2="c++";  //{'c','+','+','\0'}
char a3[4]="abcd";   //Error:会自动多出来一个\0 无处存放!
  • 不允许拷贝和复制。不能把数组的内容直接拷贝给其他的数组作为初始值。也不能直接用数组给其他的数组赋值。PS:当然,有一些编译器是支持这种行为的,但是这是非标准特性,是编译器的个人行为,并非通用!

2、 很经典的复杂的数组声明

int *ptrs[10];

从右向左依次绑定。那么ptrs先绑定[10],组成了一个数组,然后*代表这个数组的元素都是 int * 类型的。所以这个是代表着10个int 指针所组成的数组的声明。

char a[10]="IloveYYW";
char &arr[9]='f';
char &d=a;
char &d[10]=a;
char &c=a[7];
cout<<c<<endl;

上面是关于对于数组的引用。实际运行显示,第二行第三行第四行都是错误的!!!!不存在引用的数组,除非直接引用数组中的某一个元素,比如第五行的做法。这就是正确的!!

int (*parr)[10]=&ptrs;
int (&carr)[10]=ptrs;

上面两句分别是指针和引用两种常见的类型。为何要放在一起呢?首先容我为你解释两句话编译后的含义:parr是代表着一个指向容量为10的名为ptrs的数组的指针。记住,只是一个指针,不是一个指针数组。这个时候的[10]只是告诉你,这个指针指向的是以初始化的值的地址开始的长度为0的空间内的数组,它是指向整个数组而不是数组的首地址,也就是数组名ptrs所指的地址。ok既然知道了这个[10]的含义,那么我们就知道了下面的引用的含义了。另外注意,()是必不可少,不然由于优先级的问题,会造成很严重的后果。下面是我的一些示例代码,应该有助于各位理解:

char a[10]="IloveYYW";
    char &c=a[7];
    cout<<c<<endl;
    char (*p)[10]=&a;
    cout<<(*p)[3]<<endl;
    cout<<(*p)<<endl;
    p++;
    cout<<(*p)<<endl;
    char *x=a;
    x++;
    cout<<*x<<endl;

3、 小小实战:成绩归档

需求:把各个学生的成绩录入后按照十分为一个层次归档。最后输出各个层次的成绩; 分析:用一个含有11个元素的数组来记录。每录入一个成绩,就把对应档次的数组元素+1,最后遍历即可; 实现:

int tar[11];
unsigned int grade;
while(cin>>grade)
{
  if(grade<=100&&grade>=0)
    ++tar[grade/10];
}
for(int i=0;i<=10;i++)
{
  cout<<i*10<<"~"<<(i+1)*10<<"分数段有"<<tar[i]<<"人  "<<endl;
}

PS:很重要的一点,千万要注意数组下标的合法性。如果出现溢出,那是很麻烦的一件事,所以最好是加上一点合法性测试,比如上面代码中的 if(grade<=100&&grade>=0)

4、 指针与数组

数组有一个很神奇的也是对新人十分不友好的特性,那就是数组名是指向数组首地址的指针。

string name[3]={"zhang ","zhao","bo"};
string *p=name;
string xp=&name[0]

上面的两个指针定义是等价的,也就是说其实数组名就等价于是第一个元素的地址,你直接用数组名给一个指针赋值,送上去的也就是第一个元素的地址,指针也只指向第一个元素,而不是整个数组所在的块。上面说过了,要指向块,必须早数组的后面跟上数组的长度大小。

char a[10]="IloveYYW";

char (*p)[10]=&a;
cout<<(*p)[3]<<endl;
cout<<(*p)<<endl;
p++;
cout<<(*p)<<endl; 

char *x=a;
x++;
cout<<*x<<endl;

char *X=&a[0];
X++;
cout<<*X<<endl;

5、 数组的迭代器(稍微区别于vector的迭代器)

数组的迭代器。头指针好说,尾后迭代器这个就不好说了。按照尾后迭代器的概念,那么就有如下:

int arr[10];
int  *end=&arr[10];

可以看出来,arr[10]是一个不存在的元素,也是最后一个元素的下一个地址。(记住:尾后迭代器不能执行解引用以及递增操作,意思是可以递减!回到最后一个元素的地址)完美符合尾后迭代器的概念。但是标准函数库里面其实已经为我们做好了准备,按照原理来说其实就是头指针是数组名,尾指针是上面的尾后迭代器的概念:

int a[10]={1,2,3,4,5,6,7,8,9,0};
int *beg=begin(a);
int *last=end(a);

再回头看一下vector的迭代器的操作:

vector<int > v;
auto b=v.begin();

聪明的你一定发现了~~ 对于数组是调用标准函数库得到的,而对于vector ,人家是标准库类型内置函数,虽然效果一样,但是这个逼格瞬间就见高低,当然,因此数组的性能要略高于vector是肯定的。但是你耐不住现在的计算机计算能力大大提高,不在乎那么点性能和容量的损失哇!!

6、 数组的指针操作

其实数组的指针操作基本就是++ -- 那些常见的玩意 一个数组的指针加上一个长度就是到了距离这个指针所指地址多远距离的地址,这很容易理解对不,毕竟指针本身是个对象,这个对象的内容是一个地址,那么好比说有一个游标,指向一栋大楼的楼层,现在这个指针指向3楼,你给这个指针+5 自然就指向8楼了。如果取出对象内的内容,就相当于是进入了八楼的房间咯,这些常规的就不多说了。下面看一个有意思的。

int a[10]={0,1,2,3,4,5,6,7,8,9};
int *p=&a[4];
cout<<*p<<endl;
cout<<*(p+2)<<"   "<<*(p-2)<<endl;
int j=p[2];
int k=p[-2];
cout<<j<<"   "<<k<<endl;

想不到还有这种骚操作吧。不过这种也只能对着指向数组某个元素的指针用。具体的思维是参照坐标系中的相对坐标。当你重新定向坐标原点(首地址,也就是代码中的p),其实就相当于在平行的平面内重定一个坐标系。进行相对的偏移,虽然长度不变,但是范围却变了。好比上面的,数组的范围从0~9 变成了 -4~5; 你难道不觉得p这个对象跟a这个对象是一个德行吗?本身是指针,但是只要又可以加后缀,区别只是a加后缀默认取出元素,p加后缀却是只表示了偏移量而没有取数操作而已!! Sexy Operation 吧!!

7、使用数组初始化vector对象(书中还有些老掉牙的C字符串风格与string的操作,但是我觉得没啥用,就不写了)

int arr[3]={1,2,3};
vector<int > ivec(begin(arr),end(arr));

与前面介绍的vector的初始化都不一样,这次是两个迭代器来初始化。很是神奇,而且得益于指针。我们可以用vector截取数组的一部分,也就说用指针的某一段的头指针和尾后指针来初始化一个vector对象。当然,现代C++应该尽量使用vector以及内置函数和迭代器来避免数组和指针的使用。避免使用基于C风格的字符串。

8 、 多维数组的初始化

严格来说,C++没有多维数组,所谓的多维数组,其实是由数组的元素是数组这种形式变化而来。

⑴ 分行进行初始化
int a[2][3]={{1,2,3},{4,5,6}};

  在{ }内部再用{ }把各行分开,第一对{ }中的初值1,2,3是0行的3个元素的初值。第二对{ }中的初值4,5,6是1行的3个元素的初值。相当于执行如下语句:

int a[2][3];
a[0][0]=1;
a[0][1]=2;
a[0][2]=3;
a[1][0]=4;
a[1][1]=5;
a[1][2]=6;

  注意,初始化的数据个数不能超过数组元素的个数,否则出错。

⑵ 不分行的初始化
int a[2][3]={ 1,2,3,4,5,6};

  把{ }中的数据依次赋给a数组各元素(按行赋值)。即

a[0][0]=1; 
a[0][1]=2;
a[0][2]=3;
a[1][0]=4;
a[1][1]=5;
a[1][2]=6;
⑶ 为部分数组元素初始化
int a[2][3]={{1,2},{4}};

  第一行只有2个初值,按顺序分别赋给a[0][0]和a[0][1];第二行的初值4赋给a[1][0],其它数组元素的初值为0。

static int a[2][3]={ 1,2};

只有2个初值,即 a[0][0]=1,a[0][1]=2,其余数组元素的初值均为0。  

⑷ 可以省略第一维的定义,// * * 但不能省略第二维的定义 * *//。

系统根据初始化的数据个数和第2维的长度可以确定第一维的长度。

int a[ ][3]={ 1,2,3,4,5,6};

a数组的第一维的定义被省略,初始化数据共6个,第二维的长度为3,即每行3个数,所以a数组的第一维是2。例如, inta[][3]={1,2,3,4};等价于: inta[2][3]={1,2,3,4};若分行初始化,也可以省略第一维的定义。下列的数组定义中有两对{ },已经表示a数组有两行。 staticinta[][3]={{1,2},{4}};

9、 多维数组的有意思的玩法

注意注意,前面说了:数组的数组名就是数组首地址的指针。那么,多维数组好比是a[2][3],那么a[0],a[1]也是指针!!! 有意思吧!!!所以对于指针来说,可以定义指向外层数组的指针,也可以定义指向内层数组的指针,

int a[2][3]={0,1,2,3,4,5};
int (*p)[3]=a;
cout<<(*p)[2]<<endl;
p++;
cout<<(*p)[2]<<endl;

可见,p所指的是第一层,而不是第一层第一个。那么p++自然就是移到第二层了。然后再在第二层解引用之后由下标取值即可!!

如果再改变一下:

int a[2][2][2]={0,1,2,3,4,5,6,7};
int (*p)[2][2]=a;
cout<<(*p)[1][1]<<endl;
p++;
cout<<(*p)[1][1]<<endl;
int (*x)[2]=a[1];
cout<<(*x)[1]<<endl;

画图画错了。。集体增加了1,大家小心理解就好,忽略我的错误,我下面的解释基于这个错误的图像进行讲解

指针a所指的其实是包含了1234 四个元素的那个区块,而其内不是一个二维数组,所以在p指针的ID能够以后加两个长度元素表示其所指向的对象是一个二维数组,a[1]则表示的是包含了56这两个元素的那个块,所以重新定义p的时候在后面要加一个长度来表示所指的是一个一维数组。so~~ 完了

这个时候有一个auto的出现简直是人间福音。。看下面的代码就知道了:

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

这里一不小心就会把你坑死,因为p=ia,所以其实此时p是一个指向一维数组的指针,解引用后才是指向一维数组首地址的指针,再次解开引用才是真正意义上的a[0][0]

正文之后

哎?,说好的写简书只花三个小时内的时间,吃完午饭在图书馆玩了一会,处理了下年级重修的工作后开始写,写到现在五点多了。少说去了四个小时!!!我这是干了什么?!!!!!不行!!我要好好开始看书了!!今日简书封笔!!

明日再战!!

原文发布于微信公众号 - 工科狗和生物喵(gh_3507b116a1f8)

原文发表时间:2017-11-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏猿人谷

快速排序

今天介绍快速排序,这也是在实际中最常用的一种排序算法,速度快,效率高。就像名字一样,快速排序是最优秀的一种排序算法。 思想 快速排序采用的思想是分治思想。 快速...

217100
来自专栏CDA数据分析师

Python迭代和迭代器详解

一个对象,物理或者虚拟存储的序列。list,tuple,strins,dicttionary,set以及生成器对象都是可迭代的,整型数是不可迭代的。如果你不确定...

21790
来自专栏IT派

这段代码很Pythonic | 相见恨晚的 itertools 库

最近事情不是很多,想写一些技术文章分享给大家,同时也对自己一段时间来碎片化接受的知识进行一下梳理,所谓写清楚才能说清楚,说清楚才能想清楚,就是这个道理了。

9530
来自专栏我杨某人的青春满是悔恨

“身首异处”的序列(Swift)

声明:文章开头部分内容翻译自objc的一篇博客。当然,我并没有逐行翻译原文,只是说个大致意思,顺带阐述一些自己的理解和扩展思考,还有我自己的代码。

14620
来自专栏落影的专栏

程序员进阶之算法练习(三十四)LeetCode专场

LeetCode上的题目是大公司面试常见的算法题,今天的目标是拿下5道算法题: 1、2、3题都是Medium的难度,大概是头条的面试题水准; 4、5题是Hard...

13730
来自专栏aCloudDeveloper

VC库中快排函数的详解

Author: bakari  Date:  2012.8.9 以前都是自己手动写这个算法,觉得也不是一件很麻烦的事,但现在写的程序基本上都用得着快排,重新去写...

22570
来自专栏大数据和云计算技术

归并排序

算法是基础,小蓝同学准备些总结一系列算法分享给大家,这是第三篇《归并排序》,非常赞!希望对大家有帮助,大家会喜欢! 前面系列文章: #算法基础#选择和插入排序 ...

32350
来自专栏云霄雨霁

Java--集合类之Vector、BitSet、Stack、Hashtable

18370
来自专栏码云1024

JAVA 第二天 基本数据类型

27890
来自专栏苦逼的码农

Unicode与UTF-8的区别

要弄清Unicode与UTF-8的关系,我们还得从他们的来源说起,下来我们从刚开始的编码说起,直到Unicode的出现,我们就会感觉到他们之间的关系

91220

扫码关注云+社区

领取腾讯云代金券