目录
前言
这三个基础的概念,虽然每本书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.感想
无论书上还是网上的知识都是别人的,自己理解了才真正属于自己,不能好高骛远!