【C++的探索路3】数组与指针
2017-10-25 20:25
190 查看
前言
数组与指针为结构化编程中相当重要的内容,这两部分经常与引用和函数相互结合,但混起来一起讲的话可能篇幅较长容易晕。对于数组与指针这部分,主要面对的内容如下述:数组
数组的定义形式:
定义:
类型 数组名[数组长度1]...[数组长度n];类型:
一维数组,二维数组...n维数组,前面的n对应上面[ ]中的n,有几个维数就有几个[ ]。一般来说不会定义到三维数组,因为真是太。。。。繁琐了,在图像处理等领域最常见的应该就是二维数组,分别表示横纵像素的位置。
应该在意的一些细节:
初始化
数组的赋值是从[0]下标开始的,而不是1。如果不注意这一点将造成地址错误,内存泄露的问题。对于一维数组来说可以赋值少于数组所需个数的值。
比如: int a[3]={2,3};则a[0]=2,a[1]=3,a[2]=0;
此外一维数组可以省略包含多少个数量的值进行直接赋值,比如int a[]={4,5,7};则cout<<a[2]<<endl;输出的是7。
对于二维数组的话必须写明所需的列数,这个原理其实和一维数组一样。
不要在函数内部定义大数组
如【C++的探索路2】部分所讲述的内容,函数的内存空间为临时的位置,定义在栈这个位置上。在临时存储的位置定义大数组好比在临时停车场停了一辆重型坦克,很容易破坏栈。比如说在函数内部定义 int a[10000];一般来说编译器会直接蹦出一个stack crack之类的错误。如果实在需要定义一个大数组,把它扔到所有函数之外,你的编译器会非常感谢你的所作所为。
数组做函数参数
主要就两点:第一点:可以不写参数个数,二维数组需要写列数,这一点在上面的初始化部分进行了讲述。
第二点:数组作为函数参数是传引用
对于上述,可见下列程序:
void InsertionSort(int a[], int n) { int i; for (i = 0; i < n - 1; ++i) { int tmp = a[i + 1]; int j = i; while (j >= 0 && tmp < a[j]) { a[j + 1] = a[j]; --j; } a[j + 1] = tmp; } } void PrintArray(int a2d[][5], int n) { for (int i = 0; i < n; ++i) { for (int j = 0; j < 5; ++j) cout << a2d[i][j] << " "; cout << endl; } } int main() { int b[5] = { 50,20,30,40,10 }; int a2d[3][5] = { {5,2,3,1,4},{10,20,50,40,30},{100,120,50,140,30} }; InsertionSort(b, 5); for (int i = 0; i < 5; ++i) cout << b[i] << " "; cout << endl; for (int i = 0; i < 3; ++i) InsertionSort(a2d[i], 5); PrintArray(a2d, 3); return 0; }
本段程序共运用了两个知识点,其一为插入排序,其二为数组传参的使用
对于插入排序,其原理为:从右往左,依次取有序部分的元素和待插入元素比较,如果大于,则将该元素右移。。。
对于数组传参的使用,我们在PrintArray函数的定义可以看到,对于二维数组的打印,我们需要指定它的列数,不然我们无法确定地址位置。如果形参写成int a2d[3][]则会出现报错。
本部分小结如下
指针
指针为C++语言中非常重要的部分,无论是结构化还是面向对象中都有非常重要的应用,该部分的主要结构陈列如下:本小节将分五个部分进行陈述,依次为基本概念,作用与副作用,空指针与void指针,常量指针以及函数指针。
基本概念
指针与指针变量
书中定义部分存在部分的偏差,也有可能是表述存在省略,从而导致部分歧义;书中原文说:指针也称为指针变量,是一种大小为4个字节的变量,其内容代表一个内存地址。然而在Coursera上听李戈老师的授课时特意强调了下指针与指针变量的区别,为了严谨起见,本部分参考了百度百科的定义:
指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。原文链接在百度百科:指针
因此:简而言之,指针是指针,指针变量是指针变量。指针是地址,因此是一个常量值;而指针变量是一个变量。常量是不可能等同于变量的~!
那么指针变量是什么呢?指针变量是存放地址的变量。
指针变量的定义形式
T *p;即为指针变量的定义形式,其中T为*p的变量类型,p的类型则为T*。其中*为取地址符,也被称为间接引用运算符。与指针常配合的运算符为&,这个称为取地址运算符,功能是取操作数的地址。
动态内存分配
前一篇文章已经说了C++一共有四种内存区域:代码区、全局变量与静态变量区、栈与堆。其中栈又可以称为局部变量区,而堆又可称为自由存储区。在动态内存分配前先说说静态内存分配,这样才好理解什么是动,什么是静。通常我们定义的变量就是静态内存分配的,因为我们给了他的类型是什么,编译器是知道这个变量或者对象需要多少内存空间的。
而动态内存分配就不一样了,动态内存分配的变量或者对象是在编译时才确定是需要多少位置,系统根据要求进行分配内存。
说完区别来说说为什么需要动态内存分配。
动态内存分配的使用原因一是不确定,事先不确定需要多少内存区域,写到那一块的时候再根据程序员的需要进行申请,参见后面链接:用动态内存分配有什么好处啊
原因二是内存空间,堆的空间要远大于栈。综合上述两个原因,在游戏及3D领域动态内存分配应用较多。
但是既然是向组织申请的地盘,用完以后就要给他释放掉,不然走水走电的容易造成事故,而不是故事。
动态内存分配在C++中是怎么实现的呢?
如下面代码段,可以使用new操作符进行分配。
int *p; p=new int; *p=4;
当然也可以申请一大批
比如p=new int[1000];
收拾残局则用delete进行结束,回收内存。
请注意:delete只是回收内存,并不是意味着这个指针从此在地球上消失了,后续程序可以继续调用这个指针,但是将会引发严重的内存问题!!!
作用与副作用
指针最大的作用是可以不通过变量直接访问内存。这有什么用途呢?如果仅仅定义了一个变量的话,我们是无法直接对这个变量的前后内存区域进行访问的。然而如果运用指针进行运算,则可以对前后地址进行操作。但是,这也引发了一个问题,就是容易出现越界等问题, 具体例子如下:int main() { int a[3] = { 1,2,3 }; int *p = a; cout << *p << endl; cout << *(p+3) << endl; return 0; }
运行程序后cout<<*p语句正常运行,打印出1。依据常识*(p+3)将指向数组的第四个位置,然而这超过了数组的容量,最终指向了一个未知的地址,输出一个未知的数。
空指针、void指针与野指针
先从空指针与void指针讲起,中间将野指针的内容串一串。从名称上看,这两个似乎是一个玩意,空就是void,void就是空,然而。。。。本部分本计划写空指针与void指针,然而书中并没有对空指针的用途有什么用进行解释,于是乎参考了指针初始化为NULL的作用进行相应的参考修改。
空(NULL)指针
首先看看定义:空指针即用NULL关键字对任何类型的指针进行赋值,也就是值为NULL的指针;而void指针则是类型为void*类型的指针。首先我们从他们的类型上我们就知道他们不是一个东西。那么他们有什么用呢?肯定不止是单纯的照顾NULL与void这两个关键字的情怀问题。空指针的作用就是让我们知道这个指针不会瞎指,也就是不会指向任何有意义的东西,在动态内存分配的释放收尾工作当中用的比较多。比如我们在delete掉一块动态内存后,其原包含的指针的内存空间是被释放掉了,But,这个指针还是存在的!如果我们继续对这个指针进行使用,则将导致严重的内存问题,也就是所谓的野指针。
char*p = (char*)malloc(100); strcpy(p, "hello"); free(p); if (p != NULL) strcpy(p, "world");
比如上面的if判断语句就是给摆设,free掉以后,p指向一个随机的地方。
那么我们应当怎么办?当然是给他个NULL满足他~
通用(void*)指针
void指针又称作通用指针,通用指针顾名思义就是万金油,任何类型的东西都可以对他进行赋值,然后他可以通过强制类型转换再次赋值为其他类型值,这一做法在内存操作(比如memset.memcpy等)中很常见举个void指针的例子
int n = 3, *p; void*gp; gp = &n; p = (int*)gp; cout <<* p << endl;
上面只是单纯的一个例子,并没有什么卵用,在cstring头文件中的memset函数的形式为:void*memset(void*dest,int ch,int n)。
野指针
野指针在上述已经举了一个例子就是在delete或者free掉内存后,这个指针如果没有赋值为空指针,则容易瞎指,我们对这个玩意进行使用也就是用了野指针。野指针还有一种成因是指针变量没有进行初始化操作。从野指针的诞生历程,可以得出一个结论:指针有风险,用时需谨慎。
常量指针
常量指针,就是做常量而不是做变量的指针。通俗的讲就是一个小受,只允许其他人修改它,它因为怂不敢也不能去修改其他的变量。学术一点就是:不能通过常量指针去修改其指向的内容。void Func(char*p) {} void Func2(const char*p) {} int main() { const char *cp = "this"; Func(cp); //报错,因为怂,不敢动char* Func2(cp);//可以用,一家人不说两家话 char *p; cp = p;//没问题,小受而已 p = (char*)cp;//也可以,强制类型转换,逆转小受性格 char sz[20]; cp = sz;//没问题,小受可以多次受 return 0; }
如果你认为常量指针就这一种形式,too naive!常量指针是三胞胎
第二种形式是char *const p;也就是所说的常变量
该常量指针只能在初始化时指向某处,然后再也不能指向其他地方,而不像上面的const char*cp一样花心。
char c1, c2; char*const p = &c1; *p = 'a'; p = &c2;
第三种const指针就是const char* const p的形式,较为罕见。
函数指针与指向指针的指针
函数指针
函数指针部分书籍写的略微简单,实例化程度不是太足,本小段部分参考了链接函数指针及其的运用(上)--何为函数指针可惜该系列没有给出下集。
首先我们看一个计算不同几何形状面积的程序
#include <iostream> using namespace std; //函数声明 double triangle_area(double &x, double &y);//三角形面积 double rectangle_area(double &x, double &y);//矩形面积 double swap_value(double &x, double &y);//交换值 double set_value(double &x, double &y);//设定长宽(高) double print_area(double &x, double &y);//输出面积 //函数定义 double triangle_area(double &x, double &y) { return x*y*0.5; } double rectangle_area(double &x, double &y) { return x*y; } double swap_value(double &x, double &y) { double temp; temp = x; x = y; y = temp; return 0.0; } double print_area(double &x, double &y) { cout << "执行函数后:\n"; cout << "x=" << x << " y=" << y << endl; //coming soon in e.g.2... return 0.0; } double set_value(double &x, double &y) //注意参数一定要定义成引用,要不是传不出去哈! { cout << "自定义长宽(高)为:\n"; cout << "长为:"; cin >> x; cout << "宽或者高为:"; cin >> y; return 0.0; } int main() { bool quit = false;//初始化退出的值为否 double a = 2, b = 3;//初始化两个参数a和b char choice; while (quit == false) { cout << "退出(q); 设定长、宽或高(1); 三角形面积(2); 矩形面积(3); 交换长宽或高(4)." << endl; cin >> choice; switch (choice) { case 'q': quit = true; break; case '1': set_value(a, b); print_area(a, b); break; case '2': print_area(a, b); cout << "三角形的面积为:\t" << triangle_area(a, b) << endl; break; case '3': print_area(a, b); cout << "矩形的面积为:\t" << rectangle_area(a, b) << endl; break; case '4': swap_value(a, b); print_area(a, b); break; default: cout << "请按规矩出牌!" << endl; } } return 0; }
利用函数指针后,程序可以改写为
#include <iostream> using namespace std; //函数声明 double triangle_area(double &x, double &y);//三角形面积 double rectangle_area(double &x, double &y);//矩形面积 double swap_value(double &x, double &y);//交换值 double set_value(double &x, double &y);//设定长宽(高) // double print_area(double &x,double &y);//输出面积 double print_area(double(*p)(double&, double&), double &x, double &y);//利用函数指针输出面积 //函数定义 double triangle_area(double &x, double &y) { cout << "三角形的面积为:\t" << x*y*0.5 << endl; return 0.0; } double rectangle_area(double &x, double &y) { cout << "矩形的面积为:\t" << x*y << endl; return 0.0; } double swap_value(double &x, double &y) { double temp; temp = x; x = y; y = temp; return 0.0; } double print_area(double(*p)(double &x, double &y), double &x, double &y) { cout << "执行函数前:\n"; cout << "x=" << x << " y=" << y << endl; //it is coming!... p(x, y); cout << "函数指针传值后:\n"; cout << "x=" << x << " y=" << y << endl; return 0.0; } double set_value(double &x, double &y) //注意参数一定要定义成引用,要不是传不出去哈! { cout << "自定义长宽(高)为:\n"; cout << "长为:"; cin >> x; cout << "宽或者高为:"; cin >> y; return 0.0; } int main() { bool quit = false;//初始化退出的值为否 double a = 2, b = 3;//初始化两个参数a和b char choice; //声明的p为一个函数指针,它所指向的函数带有梁个double类型的参数并且返回double double(*p)(double &, double &); while (quit == false) { cout << "退出(q); 设定长、宽或高(1); 三角形面积(2); 矩形面积(3); 交换长宽或高(4)." << endl; cin >> choice; switch (choice) { case 'q': quit = true; break; case '1': p = set_value; print_area(p, a, b); break; case '2': p = triangle_area; print_area(p, a, b); break; case '3': p = rectangle_area; print_area(p, a, b); break; case '4': p = swap_value; print_area(p, a, b); break; default: cout << "请按规矩出牌!" << endl; } } return 0; }
为了避免麻烦,可以在程序第一行利用typedef进行重命名
typedef double (*vp)(double &,double &);
则
double (*p)(double &,double &);
就可以写成
vp p;
的简单形式了。
从上面两大段程序可以看到,函数指针为程序的编写提供了便利:
在定义了p函数指针后
double(*p)(double&,double&);
依次可用set_value,triangle_area,rectangle_area进行重复赋值,如果我们只是单纯的使用函数时则得不到这种便利。
另一个例子就是C语言中的qsort排序函数
qsort函数是cstdlib中的快速排序标准库函数
void qsort(void*base, int nelem, unsigned int width, int(*pfCompare)(const void*, const void*));
这个函数可以使用户自己定义使用规则,参数表内部第一个base为接受待排序数组的地址,width则为元素的个数,pfCompare为一个函数指针,指向比较函数
qsort规定,比较函数的原型为int 函数名(const void*elem1,const void*elem2),如果该函数返回负值时,*elem1应该在elem2的前面,& vice versa,贴一个比较尾数大小的程序:
#include <iostream> using namespace std; int pfCompare(const void*elem1, const void*elem2) { //依据比较规则,如果返回负数,则elem1在前面,如果要比较尾数的大小,则应该取余相减 unsigned int*p1, *p2; p1 = (unsigned int*)elem1; p2 = (unsigned int*)elem2; return (*p1 % 10) - (*p2 % 10); } int main() { const int NUM = 5; unsigned int a[NUM] = { 8,123,11,10,4 }; qsort(a, NUM, sizeof(unsigned int), pfCompare); for (int i = 0; i < NUM; ++i) cout << a[i] << " "; return 0; }
如果我们要从大到小排列,则把return(*p1%10)-(*p2%10)中p1和p2反个个就行了。
从而,函数指针的第一大优势就是提供了良好的灵活性,也就是更加便捷,比如说qsort函数可以利用函数指针使得用户自己定义排序的规则,如果只是定义了一个函数作为比较参,则非常麻烦。
函数指针的第二大优势则为具备较好的封装性,函数指针与函数同名是不会引发重复定义的问题的。
指向指针的指针
指针也是变量,因此存在(指向指针)的指针。指向指针的指针经常与二维数组或者字符数组相结合。先来个指向指针的指针基本形式
#include <iostream> using namespace std; int main() { int **pp; int *p; int n = 1234; p = &n; pp = &p; cout << *(*pp) << endl; return 0; }
与指针无异,基本方法相同
再来个指向字符串数组的指向指针的指针
#include <iostream> using namespace std; int main() { char *s[] = { "char","nani","wocao" }; char **p; p = s; cout << *p<<endl; cout << **(p + 2)<<endl; cout << *(*p+2); return 0; }
如果只是简单的定义了一个char s[]数组则只能在数组中赋值char,而无法赋值字符串,因此类型首先定义为char* []
类似于普通数组操作,对于一维数组 int a
而言,若要进行指针赋值,则可以定义 int *p; p=a;后续用p指针进行操作。
这里变成了相当于二维数组的形式,于是可以使用**p进行操作赋值。
*p获得首个元素char,而p+2指向第三个元素的地址,*(p+2)则为wocao,**则再次降级,变成了w
对于*(*p+2)得出的a,其推导过程如下:
首先*的优先级大于+,*p+2得到char中a的地址,*(*p+2)顺理成章的成了a
指针部分小结如下
整体总结如图所示
到此为止,C++的探索路的前三篇文章对涉及面向过程的内存与空间,引用和函数,数组及指针几个较为重要的环节进行了大致的总结与编程示例。下面将对面向对象部分的内容进行总结与相应的练习。第一次连续写连载性的总结文章,有谬误的地方请指正
相关文章推荐
- 数组与指针的艺术--深入探索c/c++数组与指针的奥秘
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- [转] C/C++数组名与指针区别深入探索
- [转帖] C/C++数组名与指针区别深层探索
- [收藏]C/C++数组名与指针区别深层探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- [收藏]C/C++数组名与指针区别深层探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索
- C/C++数组名与指针区别深入探索(转)
- C/C++数组名与指针区别深入探索