您的位置:首页 > 编程语言 > C语言/C++

【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++的探索路的前三篇文章对涉及面向过程的内存与空间,引用和函数,数组及指针几个较为重要的环节进行了大致的总结与编程示例。下面将对面向对象部分的内容进行总结与相应的练习。第一次连续写连载性的总结文章,有谬误的地方请指正
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息