把《c++ primer》读薄(4-2 c和c++的数组 和 指针初探)
督促读书,总结精华,提炼笔记,抛砖引玉,有不合适的地方,欢迎留言指正。
问题1、我们知道,将一个数组赋给另一个数组,就是将一个数组的元素逐个赋值给另一数组的对应元素,相应的,将一个vector 赋给另一个vector,也是将一个vector 的元素逐个赋值给另一vector 的对应元素:
//将一个vector 赋值给另一vector,使用迭代器访问vector 中的元素 vector<int> ivec(10, 20); vector<int> ivec1; for (vector<int>::iterator iter = ivec.begin(); iter != ivec.end(); iter++) { ivec1.push_back(*iter); }
问题2、编写程序判断两个数组是否相等,然后编写一段类似的程序比较两个vector。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using std::string; 5 using std::vector; 6 using std::cin; 7 using std::cout; 8 using std::endl; 9 10 int main(void) 11 { 12 const int size_array = 5; 13 int arr1[size_array]; 14 int arr2[size_array]; 15 16 cout << "输入数组 arr1的元素,个数为" << size_array << endl; 17 18 for (int i = 0; i != size_array; i++) { 19 cin >> arr1[i]; 20 } 21 22 cout << "输入数组 arr2的元素,个数为" << size_array << endl; 23 24 for (int i = 0; i != size_array; i++) { 25 cin >> arr2[i]; 26 } 27 28 //比较大小 29 for (int i = 0; i != size_array; i++) { 30 if (arr2[i] != arr1[i]) { 31 cout << "数组 arr1和2不相等!" << endl; 32 33 return 0; 34 } 35 } 36 37 cout << "数组 arr1和2相等!" << endl; 38 39 return 0; 40 }
输入数组 arr1的元素,个数为5
1
2
3
4
5
输入数组 arr2的元素,个数为5
1
2
3
4
5
数组 arr1和2相等!
Program ended with exit code: 0
or
输入数组 arr1的元素,个数为5
1
2
3
1
2
输入数组 arr2的元素,个数为5
1
2
3
4
5
数组 arr1和2不相等!
Program ended with exit code: 0
vector 容器比较大小,这注意,vector 容器和数组的不同之处!vector 容器动态变大小,可以求的长度,可以在尾部直接插入元素。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using std::string; 5 using std::vector; 6 using std::cin; 7 using std::cout; 8 using std::endl; 9 10 int main(void) 11 { 12 vector<string> svec1; 13 vector<string> svec2; 14 string str; 15 16 cout << "输入字符串,作为容器 vector1 的元素,输入 off 结束!" << endl; 17 cin >> str; 18 19 while(str != "off") 20 { 21 svec1.push_back(str); 22 cin >> str; 23 } 24 25 cout << "输入字符串,作为容器 vector2 的元素,输入 off 结束!" << endl; 26 cin >> str; 27 28 while(str != "off") 29 { 30 svec2.push_back(str); 31 cin >> str; 32 } 33 //比较长短,对于 vector 容器,需要注意下,容器是可变长的 34 if(svec1.size() != svec2.size()) 35 { 36 cout << "两个容器不等!" << endl; 37 } 38 //要么长度相等,要么不等,如果相等,则假如都为0 39 else if (svec2.size() == 0) 40 { 41 cout << "两个容器相等!" << endl; 42 } 43 //如果相等且不为0,则比较元素 44 else 45 { 46 vector<string>::iterator iter1; 47 vector<string>::iterator iter2; 48 iter1 = svec1.begin(); 49 iter2 = svec2.begin(); 50 51 while(*iter1 == *iter2 && iter1 != svec1.end() && iter2 != svec2.end()) 52 { 53 iter2++; 54 iter1++; 55 } 56 //如果元素都相等的话,必然是两个迭代器都同时指向了末位的后一位 57 if(iter1 == svec1.end()) 58 { 59 cout << "容器相等!" << endl; 60 } 61 else{ 62 cout << "容器不等!" << endl; 63 } 64 } 65 66 return 0; 67 }
问题3、指针的理解,以及和迭代器的区别
指针是一种复合数据类型,指向某种类型的对象,可以作为数组的迭代器。指针用来指向单个的对象,可以通过指针间接访问对象,而迭代器只能访问容器内的元素!
指针通俗的理解:
指针就是一个变量(一般说是一个数据对象),只不过变量的内容是地址。比如int类型变量内容是整数,而指针变量内容是地址,仅此而已。没什么神秘的!
p = &q;//p是一个变量,&q是一个常量存储了q的地址,赋值给p,我们说p指向了q。
val = *p;// *叫做间接运算符(也叫取值运算符),可以获取p地址中存放的数值,val得到了p指向的值。
因为p存放的是这块内存的地址,而不是这块内存里的数值,故要想得到这块内存里的数值,就要用取值运算符*p,找到这块内存的地址p,然后取出这块地址里的数值来。两句等价于:
val = q;
再次强调,现代 c++程序,应该避免使用指针还要数组!很容易出错,使得程序员不能把精力集中到业务上。现代 c++程序采用 vector 类型和迭代器替代一般情况下的数组的操作,使用标准库 string 类型取代 c 风格的字符串。
指针的声明
//取值(间接)运算符*表面了变量是指针变量 //类型标识符说明了被指向变量的类型 int *pi;//pi是一个指向int类型的指针,但是*pi是int类型 char *pc;//pc是一个指向char类型的指针,*pc是char类型的。 pc的值是一个地址,而pc所指向的值(*pc)是char类型,那么指针pc本身也是一种类型,叫做指向char的指针类型。
注意:指针的值是一个地址,一般用无符号整数表示,但是指针绝对不是一个整数类型。不要和处理整型相混淆。只要记住:指针是一种新的数据类型!
指针作为函数参数,指针可以实现函数间的通信
1 void change(int *x, int *y);//指针变量作为参数 2 int main() 3 { 4 int x = 5, y = 6; 5 printf("原来的:x=%d, y=%d", x, y); 6 change(&x, &y);//把主调函数里x和y的地址传递到函数作为参数,这样修改了函数的变量,因为传递的是主调函数的变量的地址而不是主调函数里的数值。这样在被调函数里操作(交换)的就是主调函数的变量内容了。 7 printf("\n改变的:x=%d, y=%d", x, y); 8 system("pause"); 9 return 0; 10 } 11 void change(int *x, int *y) 12 {//等价于变量交换,x和y都是地址,必须加*,就是本身存储的数值了 13 int temp; 14 temp = *x;//把指针x(地址)指向的内容(main函数里的变量x值)赋值给temp(俗称temp指向了x) 15 *x = *y;//地址y的内容赋值给地址x的内容 16 *y = temp;//把指针x指向的内容赋值给了地址y里的内容 17 }
函数参数传递的是x和y的地址,而不是x和y本身的内容,可以改变调用函数里变量的值。
注意:指针的使用之前,必须初始化,否则极其容易导致程序奔溃(尤其是接引用操作),发生运行时错误!可以初始化为一个对象地址,或者 NULL(0),或者另一个指针。但是直接给指针赋值整型变量是非法的!如:
int a; int zero = 0; const int b = 0; //int *ptr = a;error //int *ptr = zero;//error,int 类型的变量不能赋值给指针,即使=0也不行 int *ptr = b;//ok,可以被常量0赋值,等价 NULL int *p = 0;//ok,直接为0是可以的,或者为 NULL,这里最规范的写法是 NULL,不要使用0做空指针赋值,不规范! int *pi = NULL;
NULL是 c++从 c 继承而来的,定义在 cstdlib 头文件里,值就是0,属于预处理器变量,编译时被0替换。且注意:预处理器变量没有在 std 命名空间里定义!不用加 std::NULL。
指针只能被同类型的变量地址或者指针初始化,以及赋值!
必须保证指针的类型匹配,初始化或者赋值的时候尤其注意这个问题。
int i;
double* dp = &i;
非法。dp 为指向double 型对象的指针,不能用int 型对象i 的地址进行初始化。
c 和 c++里特殊的指针类型 void *
可以保存任何类型对象的地址,意思是说:该指针和某一个地址值相关,但是不清楚存储在此地址上的对象的类型!
支持的操作:和另一个指针比较,给函数传递 void* 类型指针或者返回 void* 类型的指针,给另一个 void *类型的指针赋值。但是绝对不能使用 void* 类型的指针操纵它所指向的对象。
int i = 42; void *p = &i; long *lp = &i;
具有void*类型的指针可以保存任意类型对象的地址,因此p 的初始化是合法的;而指向long 型对象的指针不能用int 型对象的地址来初始化,因此lp的初始化不合法。
问题4、两种指针声明的形式,解释宁愿使用第一种形式的原因:
int *ip; // good practice int* ip; // legal but misleading
第一种形式强调了ip 是一个指针,这种形式在阅读时不易引起误解,尤其是当一个语句中同时定义了多个变量时。
问题5、为什么指针使用之前必须初始化
因为,在C/C++语言中,无法检测指针是否未被初始化,也无法区分一个地址是有效地址,还是由指针所分配的存储空间中存放的不确定值的二进制位形成的地址。故建议在使用指针之前,必须初始化指针,更好的做法是,初始化全部的变量。避免运行时出错。
问题6、指针和引用的比较
他们都能间接的取访问另一值,但是有区别:
1、引用总是指向某个对象,定义引用必须初始化!否则报错!
2、给引用赋值,修改的是引用所关联的对象,而不是让引用和新的对象取关联!引用初始化之后,始终指向这个特定的对象(不会再改变)。
int ival1 = 1024; int ival2 = 2048; int *pi = &ival1; int *pi2 = &ival2; pi = pi2;
pi指向的 ival1对象不会改变,赋值修改的 pi 指针的值,使得指向另一个不同的对象。但原来的对象不会改变
int ival1 = 1024; int ival2 = 2048; int &pi = ival1; int &pi2 = ival2; pi = pi2;
赋值改变了 pi 引用的值—ival 1对象,而没有修改引用本身!和指针不一样!因为引用就是以前变量的别名!是一样的东西!而指针是两个东西。
总结:
使用引用(reference)和指针(pointer)都可间接访问另一个值,但它们之间存在两个重要区别:
(1)引用总是指向某个确定对象(事实上,引用就是该对象的别名),定义引用时没有进行初始化会出现编译错误;
(2) 赋值行为上存在差异:给引用赋值修改的是该引用所关联的对象的值,而不是使该引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象。给指针赋值修改的是指针对象本身,也就是使该指针指向另一对象,指针在不同时刻可指向不同的对象(只要保证类型匹配)。
问题7、二级指针
c 语言里,在函数内部形参指针动态申请了内存,那么经常用到二级指针取传递。c++里全部使用引用,安全高效!
所谓二级指针:就是指向指针的指针,指针本身也是可用指针指向的内存对象。
int ival = 1024; int *pi = &ival;//pi 指针是一级指针,指向了 int 变量 ival 的内存地址 int **ppi = π//ppi 二级指针,把指向 ival 对象的指针的地址存放在 ppi 指针变量里 //解引用操作 int *pi2 = *ppi;//解引用二级指针到一级指针,相当于pi2指针指向了 ival 对象的地址 //两次解引用才能真正访问到对象本身 int a = **ppi;
问题8、下列程序段实现什么功能?
int i = 42, j = 1024; int *p1 = &i, *p2 = &j; *p2 = *p1 * * p2; *p1 *= *p1;
该程序段使得i 被赋值为42 的平方,j 被赋值为42 与1024 的乘积。
问题9、二维数组
一个背景:分析5年的销售数据,一年12个月,一般用月为最小单位。那么需要定义一个60长度的数组么?显然不合适。其实把每年的数据单独放在一起最好了,可以设置5个数组,每组12。不过也显得繁琐,比如处理10年,20年等,就不爽了。数组的数组,也就是二维数组可以处理这种问题。
double d[5][12];//定义一个数组的数组(二维数组),5个由12个浮点数组成的数组的数组 //d[5] 是一个包含5个元素的一维数组,而其中5个元素的每一个元素又是一个包含12个浮点数的数组 //也就是说,数组d的每个元素类型都是double[12] //首元素就是d[0][0],是double类型的 //二维数组:有5行,每行包含12列。改变第二个下标是沿着行移动,改变第一个下标是沿着列垂直移动 //数组是顺序存储的 如图所示:5行,12列的二维数组
二维数组的初始化
int i[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
数组的数组进行初始化,外层的每一维数组初始化列表都用花括号括起来,而且中间也是用逗号隔开.对于一维数组的初始化问题,同样适用于二维数组,比如第一行列表里元素少了,那么剩余的元素自动初始化为0,如果多了,同样报错,而且一行和一行独立的,不影响以后的赋值。也可以省略内部的括号,只要最外面那层的括号,但是要保证正确.
int i[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };//不建议,因为多了容易出错
注意:多维数组初始化的两种方式效果不同
第一种就是内部初始化列表也带括号的
int i[2][3] = { {4, 5}, {7, 8} }; //很明显,初始化列表不够数组大小
还有一种内部不带花括号
还有一种内部不带花括号 int i[2][3] = { 4, 5, 7, 8 }; //很明显,初始化列表不够数组大小
等价于: int i[2][3] = {4, 5, 7, 8}; //注意区分!
多维(二维以上)数组
double d[2][3][4];//三维数组的声明形式,更多维也一样 //可以这样理解: //一维数组是一行的元素列表排列,可以看成一条线 //二维数组里有多行元素列表排列,可以看成一个面 //三维数组是多个二维的面一层层的摞起来,一个立方体。
通常处理一维数组用一层循环,二维数组用两层循环,多维用多层循环,以此类推,他们的初始化是一样的,大同小异。不过多维的用的不是太多。
问题10、数组和指针的联系初探
指针可以直接操作内存地址,而计算机的硬件指令很大程度依赖地址,故程序员可以用类似于计算机底层的表达方式来控制程序,更加高效的工作,并且指针可以有效处理数组,数组标记实际上是一种变相使用指针的形式。数组名就是这个数组首元素的地址
float flt[3] = {1, 2, 3}; // flt == &flt[0];//数组名是该数组首元素的地址,注意这里是等号不是赋值号。因为两者都是常量,程序运行的过程里不变 flt = &flt[0]; //error,这是错误的, error C2106: “=”: 左操作数必须为左值
问题11、指针的加法运算
指针的加法
//指针加法,SIZE值是3 short dates[SIZE]; short *pi; short index; double bills[SIZE]; double *pf; //pi = dates;//把数组地址(首元素的地址)赋值给指针变量 pi = &dates[0]; pf = bills;//有两种方式 printf("%18s %8s\n", "short", "double"); for (index = 0; index < SIZE; index++) {//pi存放dates数组的首地址,pf存放bills数组的首地址 printf("指针 + %d : %p %p\n", index, pi + index, pf + index); //打印观测指针加一操作之后的变化 }
指针 + 0:这一行打印的是数组的首地址,以后每行都是地址加一打印,16进制。
发现:0018fad4+1=0018fad6么,其他也是类似。这说明对指针+1操作等价于加上了指针所指向的对象的字节的大小,指针每加一就是意味着加上对象大小的字节一次。所以我们声明指针变量一定要说明指针指向的对象的数据类型,因为计算机需要知道存储对象用的字节数。
以上程序,pi是指向short类型的指针,pf是指向double类型的指针,指针的数值就是它指向的对象的地址,地址的内部表示方式硬件决定,很多计算机都是以字节编址的,说明计算机对每个内存字节进行编号,对于多字节的数据类型,比如double(8字节)对象的地址一般就是其首字节地址。而对指针使用间接运算符*,就嫩得到该指针指向的对象的具体的数值(不再是该对象的地址了)。
//下面的值是等价的 dates == &dates[0]; dates + 2 == &dates[2]; *(dates + 2) ==dates[2];
这些都说明c和 c++在描述数组的时候,借助了指针,他们有密切联系
int i[SIZE]; *(i + SIZE);//意思是寻址到内存中的i(数组首元素地址),然后继续找SIZE个单位,找到该地址再取出里面存放的数值
注意:间接运算符*的优先级高于+
*i + 10;//意思是整型数组i的首元素的值(一个整数)和整数10相加的和 *(i + 10);//意思是i数组的第11个元素的值
以上说明,我们可以用指针来标记数组,同样用数组也可以访问指针.两种方式各有用处,比如在数组作为函数参数的时候,这关系就很重要.
问题12、函数里的数组和指针
求一个整型数组的全部元素和的函数
分析:如果函数只需接收一个整型的数组名和这个数组的大小就能计算出和,那么函数参数应该是两个,一个是大小size,一个是接收数组名称,又知道数组名就是数组首元素的地址(数组首地址),那么必然用到指针参数来接受。
#include <stdlib.h> #include <stdio.h> int sum(int *i, int n); int main() { int in[5] = {1, 2, 3, 4, 5}; printf("%d", sum(in, 5));//打印15 system("pause"); return 0; } int sum(int *ii, int num) { int index; int total = 0; for (index = 0; index < num; index++) { total += ii[index]; //ii[index]和*(ii + index)等价的 } return total; }
注意:第一个参数把数组首地址和数组的类型传递给了函数,第二个参数把数组大小传递给函数。因为实际参数是一个数组名,故形式参数必须是匹配的指针。
当且仅当在给函数原型声明或者函数定义的时候:可以用int *i代替 int i[]
int sum(int i[], int n);
任何情况下,int *i;都表示i是指向int类型的指针
int i[];可以表示i是指向int的指针(只在函数原型声明和函数定义的时候可用),还有可以表示i是数组i的首地址。
必须掌握如下四个等价方式(函数原型声明可以省略参数名字),注意只有在函数原型声明或者函数定义头的时候int *i和int i[]是等价的。
int sum(int i[], int n); int sum(int [], int);//因为可以省略参数名,故数组名可以省去 int sum(int *i, int n); int sum(int *, int);//指针名可以省去
而函数定义的时候不能省略参数名字,必须掌握这两个形式
int sum(int *ii, int num){} int sum(int ii[], int num){}
再次注意只有在函数原型声明或者函数定义头的时候int *i和int i[]这种形式的是等价的。
需要明白的是:指针不论在什么情况下,对于采用四字节地址的计算机系统,大小都是4个字节,和其他无关,只和计算机系统有关。还是对于这个题,求数组元素和的函数sum(),我们需要知道数组的起始,通过指针来确定,也需要知道数组什么时候截至,通过大小size参数,不过,这里也可以用指针标识,标识数组的结束位置。
#include <stdio.h> #include <stdlib.h> #define SIZE 5 int sum(int *, int *); int main() { int in[SIZE] = {1, 2, 3, 4, 5}; printf("%d", sum(in, in + SIZE));//打印15 system("pause"); return 0; } int sum(int *start, int *end) { int total = 0; while(start < end)//没有大小参数了,那么就用关系判断 { total += *start; //先把首元素的值加到total,然后循环每个元素去累加 start ++;//指针是int类型的,自增之后推进到下一个元素 } return total; }
注意:在函数定义里,total += *start;语句里,开始把数组首元素的值取出加到total,然后start加一,推进到数组下一个元素,继续取出值累加到total,那么有一个问题出现,循环截止的时候,是当start++到和end相等的时候,此时判断为假,则循环终止,也就是说total += *start;语句只累加到end前一个元素么?
显然不是的。这就说明:end实际指向了数组最后一个元素之后的第一个位置。这在c里是合法的。类似 c++标准库里的 vector 容器的 end()函数。
sum(in, in + SIZE);
语句里in是数组首元素地址,而数组索引从0开始,那么加size之后实际指向的是end元素的下一位。如果指向最后一位,应为:
sum(in, in + SIZE - 1);
问题13、指针运算的优先级
一个问题:判断优先级
total += *start ++; //一元运算符*和++具有相同的优先级,结合从右向坐。注意到这里是后缀自增,那么应该是先把指针指向的数据加到total,然后指针自增,等价于循环体里的两行代码效果
如果是前缀自增,那么就是total += *++start ;指针先自增,然后再用自增之后指向的值累加到total。
如果是total += (*start )++;那就是赋值完成之后,指针指向的内容自增,而不是指针变量自增,效果是指针指向的地址不变,但里面的元素变了。
注意:经常使用的是total += *start ++;,但是为了清晰和方便,建议加括号
total += *(start ++);//效果一样的。
小结:
处理数组的函数实际上用的指针为参数,但是实际上,数组符号和指针符号都可以用。C和 C++语言中,i[SIZE]和*(i + SIZE)是等价的,不论i是数组名还是i是指针变量。不过注意:只有当i是一个指针变量的时候,才可以使用诸如:i++的运算
问题14、6种基本的指针操作
int i[SIZE] = {1, 2, 3, 4, 5}; int *p1, *p2, *p3;//声明三个指针变量 p1 = i;//把一个地址赋值给指针变量 p2 = &i[2]; printf("指针本身的值 = %p, 指针指向的内容 = %d, 指针本身的地址 = %p\n", p1, *p1, &p1);
指针之间的运算
p3 = p1 + 4;//指针加上整数 p1++;//指针自增 p2--;//指针自减 ++p2; p1 - p2;//指针相加减 p1 + p2; p1 - 2;
运算有6种(赋值,取值,取地址,和整数加减(自增自减),比较关系,做差):
1、赋值:通常用&或者数组名(需要注意,赋值的地址应和指针类型一致,比如int类型的指针,那么地址赋值的时候必须是int,不能是比如double类型,虽然c99许可了,但是还是不建议)。
2、取值:间接运算符*,取出指针指向内容
3、取地址:&可以用于指针,取出指针本身的地址
4、指针和一个整数相加或者相减:+符号,-符号, 指针的自增自减
5、两个指针做差:
通常用于数组元素两个元素之间的差值,求出元素之间的距离,单位是相应指针指向内容的类型的大小,比如int类型数组里,求指针差值为2,就是2个int类型的大小的距离。前提是同一个数组里。否则结果可能出错。 且在 c++里,两个指针做减法的结果是标准库类型ptrdiff_t(带符号的整数类型),在 cstddef 头文件定义。可以在指针上+、-0操作,指针不变。
6、指针之间的比较:可以用关系运算符,前提是两个指针类型相同。
注意:
进行指针的增量和减量计算之后,c并不保证指针总是指向有效的元素(比如数组越界),不会检测的。在数组里,C只保证指向第一个元素的指针和最后一个元素后一位的指针有效。
在对指针进行取值运算时,不要对指向最后一个元素的后一位的指针取值,尽管编译通过。
指针之间(一般都是数组的时候用,必须在同一个数组里)可以相减,但是不能相加!当然乘除就更离谱了……
int i[3] = {1, 2, 3}; int *ptr1 = &i[0]; int *ptr2 = &i[1]; ptr1 = ptr2 - ptr1;//可以相减 ptr2 = ptr2 + ptr1;// error C2110: “+”: 不能添加两个指针
问题15、int array[SIZE]和int *array的区别和联系要明白理解(接上)
int i[3] = {1, 2, 3};
i++;// error C2105: “++”需要左值
在函数的声明和定义头里,对数组操作的话,那么形式参数使用形式为int *array等价于使用int array[],当且仅当在函数原型声明和定义头里。
问题16、再次强调不能对没有初始化的指针取值!
int *p;//指针没有初始化! *p = 5;//大大的错误!5赋值给指针p所指向的内容,但是指针p并没有初始化,故指针p指向哪个地址并不明确,不知道5会存储到什么位置,这样取值操作的话,随机性太大,这样做可能对系统危害不大,可能是覆盖的以前的数据,也可能导致程序崩溃
记住:新建一个指针,系统只分配了用来存储指针本身的内存空间,并没有分配这个指针所指向的内容的存储空间,故在使用这个指针前,必须赋一个已经分配好的内存地址给指针,然后才能取值操作,否则随机危害太大。总之,不能对没有初始化的指针取值!
指针的作用一是在函数间进行通信,比如想要被调函数修改主调函数变量的值,那么必须使用指针操作。二是在处理数组的函数里使用。且理解一句话,指针就是数组的迭代器!
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!