C++教程-----C++指针(上)-----基础篇

C++教程正在更新中,具体可以查看教程目录

C和C++可以被认为是最接近硬件底层的语言,比如指针的概念,指针可以直接操作系统的内存,这样在给程序员方便的同时也引入了麻烦,指针可以直接在内存上读写数据,恰当的使用可以很方便的读写内训但是不恰当的使用内存可能会给程序带来风险,正所谓成也萧何,败也萧何,指针为C/C++在程序语言界奠定了很高的地位,同时指针的存在也为埋下了一系列的隐患。在此我们一起学习指针的相关知识。

指针初识

内存

如果要理解指针,那么必须要理解内存。内存就是一个存放数据的空间,可以视为装东西的盒子,这个盒子里可以装的下数据,就像下面的鸡蛋篮就可以代表计算机中的内存,鸡蛋篮中的鸡蛋可以代表内存中的变量:

在这里插入图片描述

这些内存每个位置都对应着编号,内存中存放着各种各样的数据,我们有时需要知道这些数据存放的位置,于是内存也像报告厅的座位一样被进行编号了,这就是所谓的内存编址。报告厅的座位遵循一个位置一个编号的原则,而内存则是按一个字节一个字节进行编址的,每个字节都有被称为内存地址的编号,比如我们在程序中定义不同的变量:

int number;
char name;

其实是在内存中申请一个int型变量宽度(此处算为4个字节,根据使用情况的不同,一个int占用的字节数并不相同,以下内容默认一个int占4个字节,一个char占1个字节)的空间,和一个char类型变量大小的内存,如下(但是真正的地址和我此时的地址并不一样,这里用这样的方式仅仅是方便编辑和查看)。

在这里插入图片描述

之后如果我们要将变量赋值,比如进行如下的赋值操作:

number = 0;
name = "J";

实际上是将这个数字存入到了我们定义变量所开辟的内存空间里,如下图:

在这里插入图片描述

既然通过之前的内容我们知道了变量是存放在开辟的内存里,那么我们也应该能找到这个变量在内存中对应的位置,取地址符:&可以将变量的地址取出。那么&number就是将变量number的地址取出,通过如下的代码来看一下变量在内存中的地址:

int main(){
	int number = 0;
	cout << "变量number在内存中的地址为: " << &number << endl;
	return 0;
} 

这段程序的输出结果为:变量number在内存中的地址为: 0x6ffe18

也就是说变量number在内存中对应的地址为0x6ffe18。

好了了解了这么多也为学习指针打了点基础。那么

什么是指针?

我们举个栗子,假如你要去买鸡蛋,你手边有一张纸条,纸条上面写着买鸡蛋要去菜市场的第几排第几个店里买,这个纸条上并没有鸡蛋,但是这个纸条上却记录着卖鸡蛋的位置,所以通过这个纸条我们知道了可以去哪里买鸡蛋,其实这个纸条就可以代表指针,纸条上并没有鸡蛋,但是通过纸条上的信息,我们可以知道鸡蛋在哪,也就是我们通过指针知道了变量的位置。

我们能现在来定义一个指向整型的指针:

int *t;

这样就定义了一个指针,其实指针就是一个普通的变量,和之前的变量并没有区别,这样的一个指针同样是在内存中声明一个四个字节的位置,命名为t,这里的变量和之前变量并没有区别,现在让这里的指针真正来存储一个东西让这里的t变成真正的指针。

t = &number;

上面的代码就是将变量number的地址赋值给t,也就是将number的地址写入到t里面,此时的t存储的就是变量numberr的地址,这里的t就是刚才栗子里的卖鸡蛋小店的位置,我们把变量t就称为指针,所以指针变量所存储的内容就是内存的地址编号那么我们这里就可以用这个指针来访问number这个变量了。

int main(){
	int number = 0;
	int *t;
	t = &number;
	cout << *t;
	return 0;
} 

此时的输出结果就是0

可以比较容易的理解,如果说指针就是一种对应关系,&number取出了number的地址并将这个地址赋值给t这个指针变量,*t就可以视为这个指针变量所对应的位置的变量的值,这样是不是感觉很绕,我们通过一个图就可以简单地理解这个事情

在这里插入图片描述

通过上面得表格,可以比较清晰得看出t对应的是number的地址,而*t就代表着这个地址所对应的变量,通过这个方式我们就通过指针来访问number这个变量了。

那么我们尝试用指针来修改变量的值:

int main(){
    int number_1 = 1; 
	int *number_2;
	number_2 = &number_1;
	*number_2 = 0;
	printf("%d", number_1);
	return 0;
}

通过上面的代码我们就通过指针修改了变量的值,输出的结果是0。

指针的指针—二级指针

什么是二级指针?我们通过之前的表格来观察以下什么是二级指针。

在这里插入图片描述

我们接下来用二级指针访问一下变量

int main(){
    int number_1 = 1;
    int *number_2;
    number_2 = &number_1;
	int **number_3;
	number_3 = &number_2;
	printf("%d", **number_3);
	return 0;
}

运行后输出的结果是1,也就是我们成功的用二级指针访问了变量。

同样根据之前了解的,我们不仅能用指针完成对对象的访问,同时还能通过指针完成对对象的修改,举个栗子,我们来看如下的代码:

int main(){
    int number_1 = 1;
    int *number_2;
    number_2 = &number_1;
	int **number_3;
	number_3 = &number_2;
	*number_2 = 0;
	printf("%d,", number_1);
	**number_3 = 2;
	printf("%d", number_1);
	return 0;
}

上面的程序执行之后输出结果为0,2这样我们成功的用指针间接的修改的对象的值,这里一级指针和二级指针都是可以修改对象的值的。

改变n-1级指针的指向

指针的指向不是在一经定义之后就一成不变的,而是可以根据使用者的使用需求适时的进行修改的,指针的指向不可以跨级:

可以通过一级指针修改零级指针(对象)的内容

可以通过二级指针修改一级指针的指向

可以通过三级指针修改二级指针的指向

……

可以通过n级指针修改n-1级指针的指向

二级指针的步长

所有类型的二级指针的类型都指向一级指针的类型,如果说一级指针类型的大小是4,那么二级指针的大小也是4

指针和数组

指针不止能访问一个元素,也能访问一个数组,我们学过最初的是通过所定义的变量的名字来访问数组,也就是如下的方式来访问数组:

int main(){
    int number[5] = {1, 2, 3, 4, 5};
    for(register int i = 0; i <= 4; ++ i){
    	printf("%d ", number[i]);
	}
	return 0;
}

同时我们还能用如下的方式访问数组:

int main(){
    int number[5] = {1, 2, 3, 4, 5};
    for(register int i = 0; i <= 4; ++ i){
    	printf("%d ", *(number + i));
	}
	return 0;
}

并且通过指针访问数组也是可行的:

int main(){
    int number[5] = {1, 2, 3, 4, 5};
    int *number_1;
	number_1 = number;
	for(register int i = 0; i < 5; ++ i){
		printf("%d ", number_1[i]);
	} 
	return 0;
}

上面个的这段代码整个数组给了指针*number_1通过这种方法我们也能访问这个数组。

当然通过指针用如下的方式访问数组也是可行的:

int main(){
    int number[5] = {1, 2, 3, 4, 5};
    int *number_1;
	number_1 = number;
	for(register int i = 0; i < 5; ++ i){
		printf("%d ", *(number_1 + i));
	} 
	return 0;
}

我们再来看两段代码:

int main(){
    int number[5] = {1, 2, 3, 4, 5};
    int *number_1;
	number_1 = number;
	for(register int i = 0; i < 5; ++ i){
		printf("%d ", *number_1);
		++ number_1;
	} 
	return 0;
}
int main(){
    int number[5] = {1, 2, 3, 4, 5};
    int *number_1;
	number_1 = number;
	for(register int i = 0; i < 5; ++ i){
		printf("%d ", *number);
		++ number;
	} 
	return 0;
}

上面两段代码第一段是可以执行的但是第二段会报错,两端代码的区别就是将打括号内的指针换成了变量名,但是改变后代码就会报错,说明指针和变量名本身是不同的,指针是指针变量而数组的名字是指针常量。

指针数组

指针数组其实本质就是数组,只不过数组中的每个元素不是字符或者数字而是之二而已,我们定义一个char型的指针数组:char *name[5]

在这里插入图片描述

好了我们现在定义一个指针数组并且尝试访问这个指针数组:

int main(){
    char *name[5] = {"Lpy_Now", "Mory", "Tom", "Jerry", "John"};
    for(register int i = 0; i < 5; ++ i){
    	printf("%s ", name[i]);
	}
	return 0;
}

我们还可以将这个指针数组定义为一个变长数组:

int main(){
    char *name[] = {"Lpy_Now", "Mory", "Tom", "Jerry", "John"};
    for(register int i = 0; i < sizeof(name) / sizeof(*name); ++ i){
    	printf("%s ", name[i]);
	}
	return 0;
}

我们知道函数sizeof可以获得变量的大小,那么我们就可以通过 sizeof(name) / sizeof(*name) 来确定这个变长数组的长度从而获得整个数组的长度,并访问整个数组,sizeof(name)获得了整个指针数组的大小,sizeof( *name)则获得了这个指针数组内每个元素的大小,二者相除就得到了变长的指针数组中元素的个数,我们也能成功的访问这个指针数组,这样就完成了类似string数组的功能。

指针数组的二级指针

在之前的学习中我们学习了二级指针,在指针数组这里二级指针同样存在。我们看一下如下的代码:

int main(){
    char *name[] = {"Lpy_Now", "Mory", "Tom", "Jerry", "John"};
    printf("name: ");
    for(register int i = 0; i < sizeof(name) / sizeof(*name); ++ i){
    	printf("%s ", name[i]);
	}
	printf("\nname_1: ");
	char **name_1 = name;
	for(register int i = 0; i < sizeof(name) / sizeof(*name); ++ i){
		printf("%s ", name_1[i]);
	}
	return 0;
}

在这里插入图片描述

通过上面的表格我们可以理解为什么二级指针等于指针数组名这样我们就可以通过二级指针访问指针数组了,切记这里的的二级指针等于指针数组名。

指针数组的初始化

指针一般初始化为NULL,在分配内存或者指向固定变量时改变其内容,指针数组一样初始化为NULL,这样如果指针为NULL,那么这个指针就可以分配,否则就说明指针已经被分配内存或者指向某一个变量,通常在使用完指针后,收回了分配的内存,比如delete后,要即使将指针赋值为NULL,便于再次使用指针,于是通过一下代码可以对指针数组进行初始化:

int main(){
	char *name[5];
	for(register int i = 0; i < 5; ++ i){
		name[i] = NULL;
	}
	return 0;
}

这样就完成了对指针数组的初始化,同时也可以用另一种方法判断指针数组是否到达结尾

int main(){
    char *name[] = {"Lpy_Now", "Mory", "Tom", "Jerry", "John", NULL};
    for(register int i = 0; name[i] != NULL; ++ i){
    	printf("%s ", name[i]);
	}
	return 0;
}

通过这种方法也能便利整个指针数组。

空指针

接下来我们来介绍一下控制空指针我们先来看(维基百科对空指针的解释)[https://zh.wikipedia.org/wiki/%E7%A9%BA%E6%8C%87%E6%A8%99]

在编程语言中,空指针(英语:Null Pointer)是一个已宣告但并未指向一个有效对象的指针,许多程序利用空指针来表示某些特定条件,例如未知长度数组的结尾或某些无法运行的操作。在可选择类型(optional type)的编程语言中,空指针通常可以跟可为Null的类型(Nullable types)和空值(Nothing value)进行比较。

空指针与未初始化的指针(uninitialized pointer)不同,空指针保证不会和有值的指针相等,而未初始化的指针则是要看所使用的编程语言或编译器而定,在部分编程语言环境下,未初始化的指针无法保证不与有值的指针相等,他可能因为存有存储器残值而指向了某个有效对象。

简而言之,空指针就是没有指向任何单元的指针,同样然和对象或者函数的地址都不可能是空指针,可以用NULL和0将指针设置为空指针,NULL我们之前介绍过,可以将指针设置为空指针,再来理解0为什么可以将指着设置为空指针,因为不存在一个变量的地址是0所以0是可以将指针变为空指针的。NULL本身是一个标准规定的宏定义,语句如下:

#define NULL 0

也就是在这个地方NULL可以近似等同于0,但是NULL并非在所有情况下都等于0,也可以指向一个不被使用的地址,在跨平台代码中尤其要注意。同时还存在零空指针和非零空指针,也就是是否将空指针指向0这个地址,也就是说这个空指针到底是一个zero null pointer还是一个nonzero null pointer这个取决于系统得实现,编译器会自动实现其中的转换。

通过和NULL或者0或者其他空指针的比较,就可以判断一个指针是否是空指针

void类型指针

对于任何类型的指针,不管是char还是int类型的指针还是其他类型的指针,指针所需要的存储空间的大小是完全相同的,那么此时不管这个指针是否有类型都已经不中重要了,在C++中提供了一个void*类型的变量,这样的指针仅仅表示一个内存地址,而至于这个指针的类型并不在意,两个void *的指针可以相互赋值,或者相互比较是否相等

获取首尾元素地址

在C++11的iterator库中提供了begin和end两个函数来确定数组守卫元素的指针,这样可以更方便地遍历数组:

int main(){
    char *name[] = {"Lpy_Now", "Mory", "Tom", "Jerry", "John", NULL};
    for(register int i = begin(name); i != end(name); ++ i){
    	printf("%s ", name[i]);
	}
	return 0;
}

new和delete

指针通常和堆空间有关,什么是堆呢?再维基百科中:

堆(英语:Heap)是计算机科学中的一种特别的树状数据结构。若是满足以下特性,即可称为堆:“给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于(或大于等于)C的值”。若母节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作根节点(root node),根节点本身没有母节点(parent node)。

现在只需要简单的知道堆是一种数据结构就可以了,同时堆也是一块内存,这块内存在内存上是不连续的,通过链表进行连接,这样的数据结构允许在运行的时候通过指针的方式申请一定的存储单元,这里申请的堆内存也被称为动态内存,至于动态内存分配的定义在维基百科中如下:

在计算机科学中, 动态内存分配(Dynamic memory allocation)又称为堆内存分配,是指计算机程序在运行期中分配使用内存。它可以当成是一种分配有限内存资源所有权的方法。

动态分配的内存在被程序员明确释放或被垃圾回收之前一直有效。与静态内存分配的区别在于没有一个固定的生存期。这样被分配的对象称之为有一个“动态生存期”。

堆内存的管理完全由写代码的程序员完成,如果需要使用堆内存,可以使用函数malloc()从堆内存中申请确定大小的内存,在使用完内存后还需要用函数free()将申请的内存归还,如果申请内存后没有归还这一部分内存,就会造成内存泄漏,也就是这块内存自己不会再使用,同时其他的变量也不能使用,就像和其他人借书,看完书后就将书扔在一边,自己也不再看,但是也不归还,这样这本书别人也无法阅读,这个栗子和内存泄漏的道理是一样的。malloc需要计算需要内存的大小并且要将内存区域进行类型转换才能使用,在C++中提供了new和delete两个函数进行堆内存的分配和释放,new如果分配成功则返回堆内存的首地址,否则返回空地址。通过以下的示例代码来学习如何用new和delete来动态申请和释放内存:
使用动态内存分配的时候一定记得即使释放分配得到的内存。

int main(){
    int *p1, *p2, *p3;
    p1 = new int; //申请一个能存放一个int的内存区域
	p2 = new int(1); //申请一个能存放一个int的内存区域并将1存入这个内存中
	p3 = new int[10]; // 申请一个能存放有十个元素的int类型的数组的区域
	for(register int i = 0; i < 10; ++ i){
		p3[i] = NULL;
	}
	for(register int i = 0; i < 3; ++i){
		p3[i] = i + 1;
	}
	*p1 = 0;
	printf("%d\n", *p1);
	printf("%d\n", *p2);
	for(register int i = 0; p3[i] != NULL; ++ i){
		printf("%d ", p3[i]);
	}
	delete p1;
	delete p2;
	delete []p3; //释放p3整个数组的内存 
	return 0;
}

查看上一篇:C++变量类型和变量的定义
查看下一篇:C++类和对象,一篇文章两小时带你理解清C++的类和对象
查看目录:C++教程目录

猜你喜欢

转载自blog.csdn.net/a1351937368/article/details/105282532