养成写程序的好习惯02:数组、指针和引用的区别

目录

 

前言

1. 数组和指针

1.1 一维数组

1.2 二维数组

1.3 数组和指针的共同点和区别

2.指针和引用

3.感想


前言

这三个基础的概念,虽然每本书C++编程语言的书都会提及,但是心里总觉得少了点什么,时时遇到BUG,总是百度了解决了,就这样过去了,这是个很不好的习惯。而且,博文代码从之后都进行验证。呵呵,光说不练假把式,指不定哪儿就跟自己想的有差别!

以后代码均在VS2013演示,因为还是小菜鸟一枚,所以有些东西不考虑那么多,所以这里我先不去纠结cout和printf,不去纠结UNICODE编码,不去纠结用#define还是常量或枚举的问题,先把基础学好,后面在慢慢探索,真是任重而道远!

1. 数组和指针

1.1 一维数组

先看下面例子: 

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

/*
函数功能:将传入的值都置为0
*/
void ClearAllValueForZero(int *pSrc, size_t length)
{
	for (int i = 0; i < length; i++)
	{
		pSrc[i] = 0;
	}
}

#define LENGTH 5
void main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int *p = new(std::nothrow) int[LENGTH]{1, 2, 3, 4, 5};
	if (p == nullptr)
	{
		return;
	}
	for (int i = 0; i < LENGTH; i++)
	{
		printf("%d", arr[i]);
	}
	printf("\n");
	for (int i = 0; i < LENGTH; i++)
	{
		printf("%d", p[i]);
	}
	printf("\n");
	ClearAllValueForZero(arr, LENGTH);
	ClearAllValueForZero(p, LENGTH);
	for (int i = 0; i < LENGTH; i++)
	{
		printf("%d", arr[i]);
	}
	printf("\n");
	for (int i = 0; i < LENGTH; i++)
	{
		printf("%d", p[i]);
	}
	printf("\n");
    delete[] p;
    p = nullptr;
}


//输出结果
12345
12345
00000
00000
请按任意键继续. . .


上面代码,完成的功能是一模一样。因为这样,曾经让我觉得数组和指针差不多嘛,随便了解了解就OK了。

为什么上述代码实现的功能是相同的。数组名作为参数传递给函数时,实际上该函数名等效为指向该数组首元素的指针。

也就是

void ClearAllValueForZero(int *pSrc, size_t length)
等价于
void ClearAllValueForZero(int src[], size_t length)

1.2 二维数组

注意:一维数组可以这样理解,但是《C和指针》中提到,二维数组有区别,示例代码如下

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

/*
* 将所传入的值置为0
*/
void ClearAllValueForZero(int **pSrc, int row, int col)
{
	for (int ir = 0; ir < row; ir++)
	for (int ic = 0; ic < col; ic++)
	{
		pSrc[ir][ic] = 0;
	}
}


#define M 2
#define N 3
void main()
{
	int arr2[M][N] = { 1, 2, 3, 4, 5, 6 };
	ClearAllValueForZero(arr2, M, N);  //错误
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("&d", arr2[i][j]);
		}
}


//错误提示
IntelliSense:  "int (*)[3]" 类型的实参与 "int **" 类型的形参不兼容
错误	1	error C2664: “void ClearAllValueForZero(int **,int,int)”: 无法将参数 1 从“int [2][3]”转换为“int **”	

从上述代码可以看出,这样连编译都通不过。为啥一维数组OK,而二维数组就出了问题。因为arr2是一个数组,数组里的每个元素还是一个数组,而arr2作为参数传递给函数时,表示的时指向数组的指针,而ClearAllValueForZero函数原型里是指向指针的指针,所以出错。通过错误提示,可以这么改:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

/*
* 将所传入的值置为0
*/
void ClearAllValueForZero(int (*pSrc)[3], int row, int col)
{
	for (int ir = 0; ir < row; ir++)
	for (int ic = 0; ic < col; ic++)
	{
		pSrc[ir][ic] = 0;
	}
}


#define M 2
#define N 3
void main()
{
	int arr2[M][N] = { 1, 2, 3, 4, 5, 6 };
	ClearAllValueForZero(arr2, M, N);  
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("%d", arr2[i][j]);
		}
		printf("\n");
}

/*
	ClearAllValueForZero(int **pSrc, int row, int col)
	改成
	ClearAllValueForZero(int pSrc[][3], int row, int col) 也是正确的
*/


//输出结果
000000
请按任意键继续. . .

这里int (*pSrc)[3],是一个指向数组的指针,数组大小为3。注意区分与int *pSrc[3]的区别,后者是指针数组,数组每个元素为指向int的指针。

CSDN论坛里,ForestDB在回帖时说:数组没有多维,只有一维,所谓多维,只是一维的递归定义。而指针只跟一维数组有关系。多级指针和“多维”数组没有任何关系。

 为什么二维数组在传递的时候要指明第二维,其实二维数组其实就是个一维数组,两者在一维数组时都是按照一行一行储存的,更高维也是。但是如果不指明第二维,编译器就会产生误解,

int arr[2][5] = {1,2,3,4,5,6,7,8};
如果以arr2[]作为参数传递给函数时,程序就理解成了arr2[2][4],其实程序有点笨啊。
当然以arr2[][]作为函数传递给函数时,程序更不理解了

第二维只不过是让程序知道在增加第1维的下标值时,要跳过多少字节。3为数组类似,要指明第二维和第三维。然后可能会问,为啥可以省略第一维大小,因为程序根据初始化的数据个数和第2维的长度可以确定第一维的长度。

虽然我平常不咋用二维数组,因为觉得麻烦,但是如果一定要用二维数组呢,如果这样函数原型里要确定第二维,那如果我传另一个数组(第二维不一致),那么这个函数就不能用了,那岂不是蛋疼。

解决方法有两种:

方法1:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

/*
* 将所传入的值置为0
*/
void ClearAllValueForZero(int **pSrc, int row, int col)
{
	for (int ir = 0; ir < row; ir++)
	for (int ic = 0; ic < col; ic++)
	{
		pSrc[ir][ic] = 0;
	}
}


#define M 2
#define N 3
void main()
{
	//动态分配
	int **arr2 = new int *[M];
	for (int i = 0; i < M; i++)
	{
		arr2[i] = new int[N];
	}

	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			arr2[i][j] = i + j;
		}
		//打印
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("%d", arr2[i][j]);
		}
	printf("\n");

	ClearAllValueForZero(arr2, M, N);

	//打印
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("%d", arr2[i][j]);
		}
	printf("\n");

	//释放
	for (int i = 0; i < M; i++)
	{
		delete[] arr2[i];
		arr2[i] = nullptr;
	}
	delete[] arr2;
	arr2 = nullptr;
}


//输出结果
012123
000000
请按任意键继续. . .

通过分配二级指针的内存,这样函数原型就可以声明为二级指针了。但是这样做有两个缺点:

(1)new之后要delete,特别麻烦,代码里为了方便就没有检查有没有new成功了,看知乎有人说其实不用new之后检查是否new成功,因为都new不出来了,这程序还有啥用。改写日志,写日志这个功能,老师催我好久了,但是目前项目里还没支持,汗,自己太懒了!这个星期就研究研究日志。

(2)代码里的动态分配内存,其实可以看出来,分配的内存不一定是连续的。这里需要注意。当然有一次分配的方法,这里不研究了。

方法2:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

/*
* 将所传入的值置为0
*/
void ClearAllValueForZero(int **pSrc, int row, int col)
{
	for (int ir = 0; ir < row; ir++)
		for (int ic = 0; ic < col; ic++)
		{
			pSrc[ir * col + ic] = 0;
		}
}


#define M 2
#define N 3
void main()
{
	int arr2[2][3] = { 1, 2, 3, 4, 5, 6 };
		//打印
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("%d", arr2[i][j]);
		}
	printf("\n");

	ClearAllValueForZero((int**)arr2, M, N);

	//打印
	for (int i = 0; i < M; i++)
		for (int j = 0; j < N; j++)
		{
			printf("%d", arr2[i][j]);
		}
	printf("\n");
}


//输出结果
123456
000000
请按任意键继续. . .

方法2的缺点:不能用[][]来访问了。

方法1和方法2参考子醉君迷的博客:https://blog.csdn.net/u013752202/article/details/49688717

1.3 数组和指针的共同点和区别

(1)之前示例代码中,将一维数组名作为参数传递给函数时,被视为指向数组首地址的指针,那么数组名就是指针吗?通过下面例子来验证:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

void main()
{
	char *p = nullptr;
	int arr[5] = { 0 };
	printf("指针大小为%d\n", sizeof(p));
	printf("数组名大小为%d\n", sizeof(arr));
}


//输出结果
指针大小为4
数组名大小为20
请按任意键继续. . .

从代码里,明显可以看出,数组名并不等于指针,但可以神似指针。代码里数组名其实可以理解为一种数据结构,当sizeof(arr)时,得到的是这个数据结构所占内存的大小。

[注意] 从代码中还可以看到,指针也占内存大小,且都为4。并且sizeof是一个操作符,并不是函数。

那数组名被看待为指针时,又会发生什么?

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

void PrintArrayNameSize(int *p)
{
	printf("数组名大小为%d\n", sizeof(p));
}

void main()
{
	int arr[5] = { 0 };
	printf("数组名大小为%d\n", sizeof(arr));
	PrintArrayNameSize(arr);
}


//输出结果
数组名大小为20
数组名大小为4
请按任意键继续. . .

从上述代码,可以看到当数组名作为参数传递给函数时,已经被当作指针了。真的跟指针一模一样了。

(2)数组名可以理解为指针常量。数组名作为参数传递给函数时,可以理解为指针,但是为什么是指针常量。见下例子:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>


void main()
{
	int arr[5] = { 0 };
	arr++;     //错误
}


//错误输出
error C2105: “++”需要左值

从代码可以看出,编译无法通过,原因这个时候指针作为指针常量,不能被修改。但是当作为函数参数时,被认为指针当然也就支持++行为了。

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>

void Test(int *p)
{
	printf("%p\n", p);
	p++;
	printf("%p\n", p);
}

void main()
{
	int arr[5] = { 0 };
	Test(arr);
}


//输出结果
0099FECC
0099FED0
请按任意键继续. . .

数组名作为函数参数时,失去了数据结构和指针常量的特性!

(3)指针和数组访问方式,我一般都用下标来访问,因为《C和指针》中提到一句话我很认同,效率和可读性,我选择后者。但是在某些场合需要高效率时候,则需要比较。

(4)总结,数组和指针的比较暂且探究这么多。后面若需要在深一步比较。

2.指针和引用

以一个小例子比较指针和引用:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"

void SwapValue(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}

void SwapPointer(int *a, int *b)
{
	if (a == nullptr || b == nullptr)
	{
		return;
	}
	int temp = *a;
	*a = *b;
	*b = temp;
}

void SwapRefer(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

void main()
{
	int a, b;
	//传值
	a = 5;
	b = 3;
	SwapValue(a, b);
	printf("a=%d,b=%d\n", a, b);

	//传指针
	a = 5;
	b = 3;
	SwapPointer(&a, &b);
	printf("a=%d,b=%d\n", a, b);

	//传引用
	a = 5;
	b = 3;
	SwapRefer(a, b);
	printf("a=%d,b=%d\n", a, b);
}


//输出结果
a=5,b=3
a=3,b=5
a=3,b=5
请按任意键继续. . .

从上述代码,可以看出只有传指针和传引用交换了值。这个例子基本上所有学C/C++的人都看过,我曾经也看过,那时候就认为指针和引用功能差不多,没有深入。

首先要解释的是,为什么传指针和传引用交换了值,而传值没有。按值作为参数传递给函数时,函数参数是实际参数的一份拷贝,在函数内进行任何操作,都是对那份拷贝进行的,函数结束后,自动释放,不影响实际参数。这个知识如果懂了,那指针和引用也很好懂。指针也是按值传递,只不过传递的是地址值,所以函数参数也是实际参数地址的一份拷贝,在函数里解引用改变了值,当然改变了实际内存的值,指针只是个地址,指向的计算机的某一块内存!而引用是内存的别名,就是第二个名字,所以按引用传递,在函数里改变同样改变实际参数。总的来说:按值(包括指针)传递,传递的是拷贝(函数结束后,这些变量也就释放了),而按引用传递,传递的是指针。

为什么要说这么多废话来解释这个,当初这些东西书上也看了,但是实际用的时候还是发现理解不深,见代码:

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"

void SwapPointer(int *p)
{
	int a = 3;
	p = &a;
}

void main()
{
	int a = 5;
	int *p = &a;
	printf("地址为%p,值为%d\n", p, *p);
	SwapPointer(p);
	printf("地址为%p,值为%d\n", p, *p);
}


//结果输出
地址为010FFB5C,值为5
地址为010FFB5C,值为5
请按任意键继续. . .

曾经天真的我,按指针传递后,改变指针的指向,该指针真改变了。。。至于为什么没改变,上面那段废话也说了。这里同样可以看出指针和引用的区别,指针可以改变自己的指向,而引用不能,初始化后就不能改变了。

在举一个例子,说明指针传递是传值(地址拷贝)。

// ConsoleTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"

void InitPointer(int *p)
{
	if (p = nullptr)
	{
		p = new int[5];
	}
}

void main()
{
	int *p = nullptr;
	InitPointer(p);
	printf("%p\n", p);
}


//结果输出
00000000
请按任意键继续. . .

这个例子中,函数里申请了内存,指针指向这块内存,但是这个指针只是拷贝,所以实际指针还是指向原来的内存。

总结下:指针和引用的异同

(1)在很多时候,可以将引用理解为指针;

(2)指针可以改变指向,而引用初始化后就不能改变。这里引用《C++ Primer》中的一段话:引用并非对象,相反的,它只是为了一个已经存在的对象所起的另一个名字。一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化。而且,引用只能绑定在对象上,而不能与一个字面值或某个表达式的计算结果绑定在一起。注意指针是对象。可以赋值和拷贝!

(3)有const 指针,但没有const 引用;有多级指针,但没有多级引用。

(4) sizeof(指针),得到的是指针大小(前面举例过指针大小为4,在64位下为8),而sizeof(引用),得到的是变量的大小

3.感想

无论书上还是网上的知识都是别人的,自己理解了才真正属于自己,不能好高骛远!

猜你喜欢

转载自blog.csdn.net/songsong2017/article/details/81625502
今日推荐