精华内容
下载资源
问答
  • C语言指针理解

    2013-06-14 10:59:26
    要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的 类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。 指针的概念  指针是一个特殊的变量,它里面存储的...
  • C语言指针理解C语言指针理解C语言指针理解
  • 理解C语言指针

    千次阅读 多人点赞 2019-03-25 14:40:45
    C语言指针理解 本人在初学的时候认为c语言中指针很好理解,但身边好多同学一直在说老师讲的指针太抽象了,看不到,摸不着,非常难理解,甚至学了4年计算机,毕业了,不少同学还说不清楚指针是什么,遇到指针的问题...

    C语言指针理解

    本人在初学的时候认为c语言中指针很好理解,但身边好多同学一直在说老师讲的指针太抽象了,看不到,摸不着,非常难理解,甚至学了4年计算机,毕业了,不少同学还说不清楚指针是什么,遇到指针的问题必定出错,这里简单介绍一下。

    引言

    c语言中有很多抽象的东西,而指针就是其中一个,学好 c 语言就要学会计算机的思维:透过表面看本质。
    比如下面一行代码中 两个 ! 运算符

    int x = 0;
    int a = !!0; //看这里
    

    表面上:第2行的两个 ! 运算符抵消了,相当于没有。但这只是结论。
    本质上:对0进行了两次 ! 运算。这才是本质。

    很多同学整理错题的时候也是只整理表面上的结论,所以导致同一类型的题目一再二,二再三 出错。


    正题

    为什么说指针好理解?首先要有抽象或者说本质的概念。

    感觉这个世界里非常多相似的东西,上了十几年的学,就算学新知识,也能在已有的知识体系上找到很多的相似的东西,学习指针时,可以拿已有的概念做参照来学。

    下面我先给它一个定义,让你能想象的出它大概是什么,干嘛的。

    指针像商品的条形码,像汽车的车牌,像寝室的门号,又像人的身份证号…等等。你可以把他理解为一个标志,它并不代表实际内容,只有一个值,但我们可以根据它来找到对应的真实存在的对象。

    同样的,指针并不保存所指对象的完整信息,但是我们可以根据一个映射(map)关系,来根据指针中的值,来找到对应真正的对象。看以下丑图:
    比如我有个程序:

    int main(){
    	fun1();
    	...
    }
    void fun1(){
    	fun2();
    	...
    }
    void fun2(){
    	...
    }
    

    那他在内存中的某一时刻(正在执行fun2函数)可能是这样的:
    在这里插入图片描述
    而你定义的变量或者指针,在内存中的的栈中,具体到某个栈帧里。
    在这里插入图片描述
    可以看到栈内存的分配非常像一只只集装箱,集装箱里又装了一个个快递…
    在这里插入图片描述
    其中栈和堆只是个名字,认为划分的两个区域,为了方便程序内存管理。

    • 栈中的内存不需要手动回收
      比如 int a = 1;就不用手动 free(a) 操作
      因为他会随着所在函数的结束,整个函数占用的栈内存会自动释放掉,实际上是编写 c 语言的人帮你释放掉了。

    • 堆内存需要手动回收,比如 xxx* p = malloc(…); 相当于在堆内存中开辟了一块空间,在栈内存中新建了一个变量,把堆内存的地址的值赋值给栈内存中的变量(指针),在使用完毕后需要手动释放掉堆内存的地址,因为不过不主动释放,虽然栈中指针所占内存被自动回收了,但堆内存并没有被标记为释放,而又无法通过栈中的值找到这块堆内存,即发生内存泄漏,这块堆内存在该程序结束前将一直无法使用。

    • 指针和实际指向的对象是两个东西。

    废话解释:

    像商品的条形码     纯净的商品是不带条形码的,可以根据条形码找到对应的商品
    像汽车的车牌		  汽车生产出来也是不带车牌的,但车牌可以方便我们识别一辆车
    像寝室的门号        寝室本不需要门号就能使用,门号只是方便我们找到他
    像人的身份证号	 人在一诞生是没有身份证号的,是出生后才有
    

    回归正题
    比如我有以下代码

    int a = 0; 					// 定义一个int类型变量a
    int* point_a;				// 定义一个指向 int类型变量 的指针
    point_a = &a;				//把 变量a 的内存地址 赋值给 point_a
    
    //以下为比喻-------------
    
    Car aodi = ....;			// 有一辆奥迪车,它的颜色是...长...宽.........
    Car* carNum;				// 生产一个车牌
    carNum = &aodi;				// 把这个车牌挂到车上
    

    其中,无需纠结比喻部分合不合理,主要看 c 代码即可。

    看一下内存中是如何分配的
    在这里插入图片描述
    可以看到指针的值其实是一个内存地址值,而不是具体值。具体的值保存在变量 a 中。
    因此,当你进行以下地址计算时:

    printf("%d", &a);				// a 变量的地址, 					0x8004
    
    printf("%d", point_a);			// 变量 point_a 的值 				0x8004
    
    printf("%d", a);				// 变量 a 的值						0
    
    printf("%d", *point_a);			// 变量 point_a 值被按照int解析后的值 0
    

    c 语言中 * &的意思:

    首先:内存中的状态只能表示 0 或 1,因此获取内存中变量的值的真正含义,需要根据类型来解析

    变量前符号简介详情
    什么都不加变量的值根据变量的类型,解析变量所在内存地址中的值
    &变量地址获得变量的内存地址
    *寻址并解析寻找地址等于变量值的内存,根据变量类型解析这块内存中的值
    以上是指针知识的大概介绍(是什么)

    大概对指针有点概念后,还需要知道他存在的意义(为什么存在)和用法(怎么用

    存在意义

    主要是为了减轻程序员的负担。

    虽然 c 还是不 java,python,js 等写起来爽,几乎不需要关心内存了,但他相比于汇编语言,机器代码,有这种思想,在当时已经非常先进和高级了。

    因为如果让程序员直接管理 地址,解析地址中的 0 或 1,那不得累死?所以出现了指针这个东西,极大地方便了程序员来管理内存,在写代码时无需关心底层内存是如何分配的,可以更好的专注于功能的设计。
    题外话
    当然在现在,c 语言毕竟使用时还需要手动释放堆内存,允许直接访问内存地址,操作不当则很容易使得系统或者其他程序出错(如修改某游戏所占内存中的值,简称外挂),内存自动分配和回收的语言更加受到开发者的喜爱。


    如何使用

    以上是本质,如果只看现象不看本质,在做题时候稍微有点坑,便反应不过来。

    比如一般写代码时候 总是让某一类型的指针指向特定类型的变量,如下

    //易读写法
    int i = 0;
    int* point_i = &i;
    
    char c = ' ';
    char* point_c = &c;
    
    //实际中经常会简写为
    int i = 0, *pi = &i;
    char c = ' ', pc = &c;
    

    其实以上都是非常规范的写法,是指针的最简单的使用,大部分出现在入门阶段。

    深入分析

    我们通过现象看本质知道。
    析地址符 * 可以按照后面变量的类型来进行解析内存等于其后变量值的内容
    假设可以以下操作

    char c = 'a';
    
    char* point_c = &c;
    short* point_s = &c;
    int*  point_i = &c;
    

    那我们就知道这三个指针变量 的值相同,类型不同,因类型不同,解析出来的表现也不同。
    ‘a’ 在内存中存的内容实际是其 ascll 码值,即 61 ,char 类型占用的内存为 1 字节即 8 位,则在内存中形式为 000111101,
    那么 *point_c 的流程:

    1. 根据 point_c 的值获得内存地址
    2. 根据 1 得到的内存地址找到对应的内存
    3. 获取找到内存的类型
    4. 检查是否为自身或为(point_c ) 类型的子类,如果不是继续判断能否进行强制类型转换
    5. 如果是,根据所得的地址,按照自身 (point_c) 类型,获取该区域内存的值(char类型,读取1个字节),000111101
    6. 按照 自身 (point_c) 类型解析(转换为 ascll 码所代表的 字符) ,转换成 ‘a’
    *point_s 的过程与上类似,我们这里做个假设第 4 步通过了,会发生什么?

    直接来到第 5. 步:
    5. 按照自身类型(point_s),从该区域读,连续读 2 个字节 000111101 xxxxxxxx
    6. 按照 short 类型解析,大部分计算机内存中内存存储会按照高位在前,低位在后存储,因此:
    - 其读到的数据用二进制表示为 xxxxxxxx 000111101
    - 把它认为成一个 short 类型数据读取

    其他情况本质上都是这样的

    提示

    初学者最重要的就是要理解 指针它所指变量个变量。


    本篇仅面向初学者,只介绍了指针的读取,有不理解的地方欢迎留言。

    展开全文
  • 深入理解C语言指针

    万次阅读 多人点赞 2019-09-28 08:36:51
    而这个编号可以暂时理解指针,就像酒店的门牌号一样。 1.1、变量和地址 先写一段简单的代码: void main(){ int x = 10, int y = 20; } 这段代码非常简单,就是两个变量的声明,分别赋值了 10、20。我们把内存...

    一、指针的概念

    要知道指针的概念,要先了解变量在内存中如何存储的。在存储时,内存被分为一块一块的。每一块都有一个特有的编号。而这个编号可以暂时理解为指针,就像酒店的门牌号一样。

    1.1、变量和地址

    先写一段简单的代码:

    void main(){
    	int x = 10, int y = 20;
    }
    

    这段代码非常简单,就是两个变量的声明,分别赋值了 10、20。我们把内存当做一个酒店,而每个房间就是一块内存。那么“int x = 10;”和“int y = 20;”的实际含义如下:

    1. 去酒店订了两个房间,门牌号暂时用 px、py 表示
    2. 让 10 住进 px,让 20 住进 py
    3. 其中门牌号就是 px、py 就是变量的地址
    4. x 和 y 在这里可以理解为具体的房间,房间 x 的门牌号(地址)是 px,房间 y 的门牌号(地址)是 py。而 10 和 20,通过 px、py 两个门牌,找到房间,住进 x、y。用户(变量的值)和房间(变量)以及房间号(指针、地址)的关系

    1.2、指针变量和指针的类型

    指针变量就是一个变量,它存储的内容是一个指针。如果用前面的例子,可以理解为指针变量就是一张房卡,房卡存储了房间号的信息。

    在我们定义一个变量的时候,要确定它的类型。int x、char ch、float、、、在定义指针变量时也是一样的,必须确定指针类型。int 变量的指针需要用 int 类型的指针存储,float 变量的指针需要用 float 类型的指针存储。就像你只能用酒店 A 的房卡存储酒店 A 中房间号的信息一样。

    二、变量的指针与指针变量

    变量的指针就是变量的存储地址,指针变量就是存储指针的变量。

    2.1、指针变量的定义及使用

    (1)指针变量的定义

    指针变量的定义形式如:数据类型 *指针名;例如:

    //分别定义了 int、float、char 类型的指针变量
    int *x;
    float *f;
    char *ch;
    

    如上面的定义,指针变量名为 x、f、ch。并不是*x、*f、*ch

    (2)指针变量的使用

    • 取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。
    • 指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。

    代码示例:

    //声明了一个普通变量 a
    int a;
    //声明一个指针变量,指向变量 a 的地址
    int *pa;
    //通过取地址符&,获取 a 的地址,赋值给指针变量
    pa = &a;
    //通过间接寻址符,获取指针指向的内容
    printf("%d", *pa);
    

    (3)“&”和“*”的结合方向

    “&”和“*”都是右结合的。假设有变量 x = 10,则*&x 的含义是,先获取变量 x 的地址,再获取地址中的内容。因为“&”和“*”互为逆运算,所以 x = *&x。

    接下来做个小练习,输入 x、y 两个整数,然后将其中的值大的赋值给 x,小的赋值给 y。即:假设输入 x = 8,y = 9。就将 9 赋值给 x,8 赋值给 y。

    void main(){
    	//声明两个普通变量
    	int x, y;
    	//声明两个指针变量
    	int *px, *py;
    	//声明一个临时变量,用于交换
    	int t;
    	//输入两个值,赋值给 x、y
    	scanf("%d", &x);
    	scanf("%d", &y);
    	//给指针变量 px、py 赋初值(关联变量 x、y)
    	px = &x;
    	py = &y;
    	//利用指针来对比 x、y 的值,如果 x 的值比 y 的值小,就交换
    	if(*px < *py){
    		//交换步骤,其中*px == x、*py == y
    		t = *px;
    		*px = *py;
    		*py = t;
    	}
    	printf("x =  %d, y = %d", *px, *py);
    }
    
    输入:23 45
    输出结果为:x = 45, y = 23
    

    2.2、指针变量的初始化

    指针变量与其它变量一样,在定义时可以赋值,即初始化。也可以赋值“NULL”或“0”,如果赋值“0”,此时的“0”含义并不是数字“0”,而是 NULL 的字符码值。

    //利用取地址获取 x 的地址,在指针变量 px 定义时,赋值给 px
    int x;
    int *px = &x;
    //定义指针变量,分别赋值“NULL”和“0”
    int *p1= NULL, *p2 = 0;
    

    2.3、指针运算

    (1)赋值运算

    指针变量可以互相赋值,也可以赋值某个变量的地址,或者赋值一个具体的地址

    int *px, *py, *pz, x = 10;
    //赋予某个变量的地址
    px = &x;
    //相互赋值
    py = px;
    //赋值具体的地址
    pz = 4000;
    

    (2)指针与整数的加减运算

    1. 指针变量的自增自减运算。指针加 1 或减 1 运算,表示指针向前或向后移动一个单元(不同类型的指针,单元长度不同)。这个在数组中非常常用。
    2. 指针变量加上或减去一个整形数。和第一条类似,具体加几就是向前移动几个单元,减几就是向后移动几个单元。
    //定义三个变量,假设它们地址为连续的,分别为 4000、4004、4008
    int x, y, z;
    
    //定义一个指针,指向 x
    int *px = &x;
    
    //利用指针变量 px 加减整数,分别输出 x、y、z
    printf("x = %d", *px);		//因为 px 指向 x,所以*px = x
    
    //px + 1,表示,向前移动一个单元(从 4000 到 4004)
    //这里要先(px + 1),再*(px + 1)获取内容,因为单目运算符“*”优先级高于双目运算符“+”
    printf("y = %d", *(px + 1));		
    printf("z = %d", *(px + 2));
    

    (3)关系运算

    假设有指针变量 px、py。

    1. px > py 表示 px 指向的存储地址是否大于 py 指向的地址
    2. px == py 表示 px 和 py 是否指向同一个存储单元
    3. px == 0 和 px != 0 表示 px 是否为空指针
    //定义一个数组,数组中相邻元素地址间隔一个单元
    int num[2] = {1, 3};
    
    //将数组中第一个元素地址和第二个元素的地址赋值给 px、py
    int *px = &num[0], *py = &num[1];
    int *pz = &num[0];
    int *pn;
    
    //则 py > px
    if(py > px){
    	printf("py 指向的存储地址大于 px 所指向的存储地址");
    }
    
    //pz 和 px 都指向 num[0]
    if(pz == px){
    	printf("px 和 pz 指向同一个地址");
    }
    
    //pn 没有初始化
    if(pn == NULL || pn == 0){
    	printf("pn 是一个空指针");
    }
    

    三、指针与数组

    之前我们可以通过下标访问数组元素,学习了指针之后,我们可以通过指针访问数组的元素。在数组中,数组名即为该数组的首地址,结合上面指针和整数的加减,我们就可以实现指针访问数组元素。

    3.1、指向数组的指针

    如以下语句:

    int nums[10], *p;
    

    上面语句定义了一个数组 nums,在定义时分配了 10 个连续的int 内存空间。而一个数组的首地址即为数组名nums,或者第一个元素的首地址也是数组的首地址。那么有两种方式让指针变量 p 指向数组 nums:

    //数组名即为数组的首地址
    p = nums;
    //数组第一个元素的地址也是数组的首地址
    p = &nums[0];
    

    上面两句是等价的。
    如下几个操作,用指针操作数组:

    1. *p = 1,此操作为赋值操作,即将指针指向的存储空间赋值为 1。此时 p 指向数组 nums 的第一个元素,则此操作将 nums 第一个元素赋值为 0,即 nums[0] = 1。
    2. p + 1,此操作为指针加整数操作,即向前移动一个单元。此时 p + 1 指向 nums[0]的下一个元素,即 nums[1]。通过p + 整数可以移动到想要操作的元素(此整数可以为负数)。
    3. 如上面,p(p + 0)指向 nums[0]、p + 1 指向 nums[1]、、、类推可得,p+i 指向 nums[i],由此可以准确操作指定位置的元素。
    4. 在 p + 整数的操作要考虑边界的问题,如一个数组长度为 2,p+3 的意义对于数组操作来说没有意义。

    下面写一段代码,用指针访问数组的元素:

    //定义一个整形数组,并初始化
    int nums[5] = {4, 5, 3, 2, 7};
    
    //定义一个指针变量 p,将数组 nums 的首地址赋值给 p,也可以用p = &nums[0]赋值
    int *p = nums, i;			//i 作为循环变量
    
    //p 指向数组第一个元素(数组首地址),我们可以直接用间接寻址符,获取第一个元素的内容
    printf("nums[0] = %d\n", *p);			//输出结果为 nums[0] = 4
    
    //我们可以通过“p + 整数”来移动指针,要先移动地址,所以 p + 1 要扩起来
    printf("nums[1] = %d\n", *(p + 1));		//输出结果为 nums[1] = 5
    
    //由上面推导出*(p + i) = nums[i],所以我们可以通过 for 循环变量元素
    for(i = 0; i < 5; i++){
    	printf("nums[%d] = %d", i, *(p + i));
    }
    

    注:数组名不等价于指针变量,指针变量可以进行 p++和&操作,而这些操作对于数组名是非法的。数组名在编译时是确定的,在程序运行期间算一个常量。

    3.2、字符指针与字符数组

    在 C 语言中本身没有提供字符串数据类型,但是可以通过字符数组和字符指针的方式存储字符串。

    (1)字符数组方式

    这个在前面应该学习过,这里就不赘述了。

    char word[] = "zack";
    printf("%s", word);
    

    (2)字符指针方式

    指针方式操作字符串和数组操作字符串类似,可以把定义的指针看做是字符数组的数组名。在内存中存储大致如下,这里为了方便换了个字符串:在这里插入图片描述

    //除了定义一个字符数组外,还可以直接定义一个字符指针存储字符串
    char *sentence = "Do not go gentle into that good night!";
    
    //此时可以做字符串的操作
    //输出
    printf("%s", sentence);
    
    //通过下标取字符
    printf("%c", sentence[0]);
    
    //获取字符串长度,其中 strlen 是 string.h 库中的方法
    printf("%d", strlen(sentence));
    

    注:字符指针方式区别于字符数组方式,字符数组不能通过数组名自增操作,但是字符指针是指针,可以自增操作。自增自减少会实现什么效果大家可以自己尝试运行一下

    下面做个小练习,利用字符指针将字符数组 sentence 中的内容复制到字符数组 word 中:

    //定义字符数组 sentence 和 word,给 sentence 赋初值
    char sentence[] = "Do not go gentle into that good night!", word[100];
    
    //定义字符指针,指向 word
    char *ch = word;
    int i;
    
    //循环赋值
    for(i = 0; sentence[i] != '\0'; i++){
    	*(ch + i) = sentence[i];
    }
    
    //在当 i 等于 sentence 的长度(sentence 的长度不包含'\0')时,
    //i 继续自增,此时判断 sentence[0] != '\0'不符合,跳出循环,则 i 比 sentence 长度大 1
    *(ch + i) = '\0';
    
    //输出字符串,因为 ch 指向 word,所以输出结果是一样的
    printf("ch = %s, word = %s", ch, word);
    

    注:指针变量必须初始化一个有效值才能使用

    3.3、多级指针及指针数组

    (1)多级指针

    指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。我们先看看二级指针,它们关系如下:指针变量 p 指向变量 x,二级指针变量指向指针变量 p
    其中 p 为一级指针,pp 为二级指针。二级指针定义形式如下:

    数据类型 **二级指针名;
    

    和指针变量的定义类似,由于*是右结合的,所以*pp 相当于*(*p)。在本次定义中,二级指针的变量名为 pp,而不是**p。多级指针的定义就是定义时使用多个“*”号。下面用一个小程序给大家举例:

    //定义普通变量和指针变量
    int *pi, i = 10;
    //定义二级指针变量
    int **ppi;
    
    //给指针变量赋初值
    pi = &i;
    
    //给二级指针变量赋初值
    ppi = &pi;
    
    //我们可以直接用二级指针做普通指针的操作
    //获取 i 的内容
    printf("i = %d", **ppi);
    //获取 i 的地址
    printf("i 的地址为%d", *ppi);
    

    注:在初始化二级指针 ppi 时,不能直接 ppi = &&i,因为&i 获取的是一个具体的数值,而具体数字是没有指针的。

    (2)指针数组

    指针变量和普通变量一样,也能组成数组,指针数组的具体定义如下:

    数据类型 *数组名[指针数组长度];
    

    下面举一个简单的例子熟悉指针数组:

    //定义一个数组
    int nums[5] = {2, 3, 4, 5, 2}, i;
    
    //定义一个指针数组
    int *p[5];
    
    //定义一个二级指针
    int **pp;
    
    //循环给指针数组赋值
    for(i = 0; i < 5; i++){
    	p[i] = &nums[i];
    }
    
    //将指针数组的首地址赋值给 pp,数组 p 的数组名作为 p 的首地址,也作为 p 中第一个元素的地址。
    //数组存放的内容为普通变量,则数组名为变量的指针;数组存放的内容为指针,则数组名为指针的指针。
    pp = p;
    
    //利用二级指针 pp 输出数组元素
    for(i = 0; i < 5; i++){
    	//pp == &p[0] == &&nums[0],nums[0] == *p[0] == **pp
    	printf("%d", **pp);
    	
    	//指针变量+整数的操作,即移动指针至下一个单元
    	pp++;
    }
    

    3.4、指针与多维数组

    讲多维数组是个麻烦的事,因为多维数组和二维数组没有本质的区别,但是复杂度倒是高了许多。这里我主要还是用二维数组来举例,但是还是会给大家分析多维数组和指针的关系。

    (1)多维数组的地址

    先用一个简单的数组来举例:

    int nums[2][2] = {
    	{1, 2},
    	{2, 3}
    };
    

    我们可以从两个维度来分析:

    1. 先是第一个维度,将数组当成一种数据类型 x,那么二维数组就可以当成一个元素为 x 的一维数组。
    2. 如上面的例子,将数组看成数据类型 x,那么 nums 就有两个元素。nums[0]和 nums[1]。
    3. 我们取 nums[0]分析。将 nums[0]看做一个整体,作为一个名称可以用 x1 替换。则 x1[0]就是 nums[0][0],其值为 1。
      在这里插入图片描述

    我们知道数组名即为数组首地址,上面的二维数组有两个维度。首先我们把按照上面 1 来理解,那么 nums 就是一个数组,则nums 就作为这个数组的首地址。第二个维度还是取 nums[0],我们把 nums[0]作为一个名称,其中有两个元素。我们可以尝试以下语句:

    printf("%d", nums[0]);
    

    此语句的输出结果为一个指针,在实验过后,发现就是 nums[0][0]的地址。即数组第一个元素的地址。

    如果再多一个维度,我们可以把二维数组看做一种数据类型 y,而三维数组就是一个变量为 y 的一维数组。而数组的地址我们要先确定是在哪个维度,再将数组某些维度看成一个整体,作为名称,此名称就是该维度的地址(这里有些绕)。

    例:

    //假设已初始化,二维数组数据类型设为 x,一维数组数据类型设为 y
    int nums[2][2][2];
    
    //此数组首地址为该数组名称
    printf("此数组首地址为%d", nums);
    
    //此数组可以看做存储了两个 x 类型元素的一维数组,则 nums[0] = x1 的地址为
    printf("第二个维度的首地址为%d", nums[0]);
    
    //而 x1 可以看做存储了两个 y 类型元素的一维数组,则 y1 = x1[0] = nums[0][0]
    printf("第三个维度的首地址为%d", nums[0][0]);
    
    

    三维数组实际存储形式如下:
    在这里插入图片描述
    实际存储内容的为最内层维度,且为连续的。对于 a 来说,其个跨度为 4 个单元;对 a[0]来说,其跨度为 2 个单元;对 a[0][0]来说,跨度为一个单元。有上面还可以得出:

    a == a[0] == a[0][0] == &a[0][0][0];
    

    上面的等式只是数值上相等,性质不同。

    (2)多维数组的指针

    在学习指针与数组的时候,我们可以如下表示一个数组:

    int nums[5] = {2, 4, 5, 6, 7};
    int *p = nums;
    

    在前面讲指针数组时,所有指针数组元素都指向一个数字,那么我们现在可以尝试用指针数组的每个元素指向一个数组:

    //定义一个二维数组
    int nums[2][2] = {
    	{1, 2},
    	{2, 3}
    };
    
    //此时 nums[0]、和 nums[1]各为一个数组
    int *p[2] = {nums[0], nums[1]};
    
    //我们可以用指针数组 p 操作一个二维数组
    
    //p 为数组 p 的首地址,p[0] = nums[0] = *p,**p = nums[0][0]
    printf("nums[0][0] = %d", **p);
    
    //指针 + 整数形式,p+1 移动到 nums 的地址,*(p +1) = nums[1],则**(p + 1) = nums[1][0]
    printf("nums[1][0] = %d", **(p + 1));
    
    //先*p = nums[0],再*p + 1 = &nums[0][1],最后获取内容*(*p + 1)即为 nums[0][1]
    printf("nums[0][1] = %d", *(*p + 1));
    

    这里可能不能理解为什么*p + 1 = &nums[0][1],而不是 nums[1]。*p 获得的是一个一维数组,而 int 数组 + 1 的跨度只有 4 个字节,也就是一个单元。前面 p 是一维数组的指针,其跨度为一个数组。所以*p + 1 = &nums[0][1],而 p + 1 = nums[1]。

    四、指针与函数

    前面学习函数学到,函数参数可以为 int、char、float 等,但是在操作时,这些参数只作为形参,所有操作都只在函数体内有效(除对指针的操作外),那么今天来学习一下指针作为函数参数。

    4.1、函数参数为指针

    我们直接做一个练习,定义一个函数,用来交换两个变量的内容。

    void swap(int *x, int *y);
    void main(){
    	int x = 20, y = 10;
    	swap(&x, &y);
    	printf("x = %d, y = %d", x ,y);
    }
    void swap(int *x, int *y){
    	int t;
    	t = *x;
    	*x = *y;
    	*y = t;
    }
    

    代码非常简单,我也就不细讲了。这里传入的参数为指针,所以调用 swap 方法后 x,y 的内容发生了交换。如果直接传入 x,y,那么交换只在 swap 中有效,在 main 中并没有交换。

    4.2、函数的返回值为指针

    返回值为指针的函数声明如下:

    数据类型 *函数名(参数列表){
    	函数体
    }
    //例如:
    int s;
    int *sum(int x, int y){
    	s = x + y;
    	return &s;
    }
    

    在函数调用前要声明需要对函数声明(有点编译器不需要)

    int s;
    void mian(){
    	int *r = sum(10, 9);
    	printf("10 + 9 + %d", *r);
    }
    int *sum(int x, int y){
    	s = x + y;
    	return &s;
    }
    

    除了上面的操作,更实用的是返回一个指向数组的指针,这样就实现了返回值为数组。

    4.3、指向函数的指针

    C 语言中,函数不能嵌套定义,也不能将函数作为参数传递。但是函数有个特性,即函数名为该函数的入口地址。我们可以定义一个指针指向该地址,将指针作为参数传递。

    函数指针定义如下:

    数据类型 (*函数指针名)();
    

    函数指针在进行“*”操作时,可以理解为执行该函数。函数指针不同与数据指针,不能进行+整数操作。

    下面举个例子,来使用函数指针:

    #include <string.h>
    /**
    *	定义一个方法,传入两个字符串和一个函数指针 p,用 p 对两个字符串进行操作
    */
    void check(char *x, char *y, int (*p)());
    void main(){
    	//string.h 库中的函数,使用之前需要声明该函数。字符串比较函数
    	int strcmp();
    	char x[] = "Zack";
    	char y[] = "Rudy";
    	
    	//定义一个函数指针
    	int (*p)() = strcmp;
    
    	check(x, y, p);
    }
    void check(char *x, char *y, int (*p)()){
    	if(!(*p)(x, y)){
    		printf("相等");
    	}else{
    		printf("不相等");
    	}
    }
    

    利用函数指针调用方法具体操作如下:

    (*p)(x, y);
    

    指针除了这些地方,还在结构体中用处巨大。今天就先讲到这里~·

    展开全文
  • C语言指针详解(通俗易懂)

    万次阅读 多人点赞 2019-04-21 10:43:25
    刚开始接触C语言,对C语言中的指针有一点不能理解,接着还有一个"&"符号感觉两者有点接近和混淆。两者的定义如下: &是取地址符号 *是定义指针变量,即指向内存单元的指针 指针是C/C++语言的特色,它允许...

    前言

    刚开始接触C语言,对C语言中的指针有一点不能理解,接着还有一个"&"符号感觉两者有点接近和混淆。两者的定义如下:

    • &是取地址符号
    • *是定义指针变量,即指向内存单元的指针

    指针是C/C++语言的特色,它允许程序员直接操纵内存,所以说C语言是一种高效的语言。很多语言屏蔽了编程人员直接操纵内存的权限去降低软件开发的难度。

    &

    #include<stdio.h>
    int main()
    {
        int a=20;
    	printf("a 的值为%d\n",a);
    	printf("a 的地址为%d\n",&a);
    	return 0;
    }
    //打印结果:
    a 的值为20
    a 的地址为1703740(1703740为a地址0019FF3C的十进制)
    

    & 符号的作用很清晰, &a就是取变量a的地址。


    *

    #include<stdio.h>
    int main()
    {
        int a=20;
    	int *ip;
    	ip=a;
    	
    
    	printf("ip 的值为%d\n",ip);
    	printf("*ip 的值为%d\n",*ip);
    	return 0;
    }
    //打印结果:
    ip 的值为20
    

    为什么只打印了一个值呢?刚开始学习不是很懂,回去查指针变量的定义,再加上网上查阅各种资料,定义说:*表示一个指针变量,指向内存单元,指向内存单元,应该就是地址吧,那么ip变量应该被赋值一个地址,前边说的 & 不就表示一个地址吗,试一下

    #include <stdio.h>
     
     
    int main ()
    {
    	int a=5;
    	int *b;
    	b=&a;
    	printf("a 的值为 %d\n",a);
    	printf("&a 的值为 %d\n",&a);
    	printf("b的值为%d\n",b);
    	printf("*b 的值为 %d\n",*b);
    }
    //打印结果:
    a 的值为 5
    &a 的值为 1703740
    b的值为1703740
    *b 的值为 5
    

    定义变量时int* a;int* 表示取指针,在地址前加*表示这个地址的内容。

    函数指针和指针函数

    函数指针的定义:
    returnType (*pointerName)(param list);
    指针函数的定义:
    returnType *pointerName(paramlist);

    returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表,函数指针赋值为函数地址。注意:( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(paramlist);就成了函数原型(指针函数),它表明函数的返回值类型为returnType *,返回值类型为1个指针。

    调用函数直接调用就好了,为什么还要定义一个函数指针呢,才接触时很不理解,后了解到由于函数指针存储的是一个内存地址,在传参的时候传入一个地址比传入一个地址对应的函数名更高效,运算速度更快。

    数组指针

    数组是一种结构类型,在某种意义上来说数组名就是指针,可以转化为指向其实体的指针,指针则是变量,仅仅意味着数组的存放地址,数组名概念比较宽泛,也更加复杂,而指针仅仅代表数组的首地址

    #include <stdio.h>
     
    int main()
    {
    	int a[]={1,2,3,4,5};
    	int *p=a;
    	printf("数组a首地址%d\n",&a);//数组a首地址1703724
    	printf("数组a首地址%d\n",p);//数组a首地址1703724
    	printf("a所占空间%d\n",sizeof(a));//a所占空间20(a代表整个数组,大小为4*5=20)
    	printf("*p所占内存空间%d\n",sizeof(p));//*p所占内存空间4(*p仅仅指向首地址)
    	printf("*p指向地址的内容%d\n",*p);//*p指向地址的内容1
    
    
    	return 0;
     
    }
    

    指向指针的指针

    #include<stdio.h>
    #include<stdlib.h>
    
    int main(){
    	
    	char *s[]={"aaa","bbb","ccc","ddd"};
    	char **p;//定义指向指针的指针
    	int k;
    	for(k=0;k<4;k++){
    		p=&s[k];//指针赋值为地址 如果不加则为s[k]的内容,会出错
    		printf("%s\n",*p);//使用*p访问指针所指向地址的内容
    	
    	}
    	return 0;
    
    }
    

    总结

    1. &a表示a变量的地址,scanf("%d",&a)中&a就是将输入的值存入a变量的地址中
    2. *ip定义一个指针变量ip,ip代表指针变量,赋值内容为地址(即指向变量的地址)
    3. *ip表示值是地址的变量,访问的是ip地址内容
    4. 在数组中指针变量仅仅代表数组的首地址,数组名就数组的首地址
    展开全文
  • C语言指针详解(经典,非常详细)

    万次阅读 多人点赞 2019-06-01 17:26:12
    要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,...

    前言:复杂类型说明

        要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我总结了一下其原则:从变量名处起,根据运算符优先级结合,一步一步分析.下面让我们先从简单的类型开始慢慢分析吧:

    1. int p; //这是一个普通的整型变量 
    2. int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针 
    3. int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组 
    4. int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组 
    5. int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针 

     

    1. int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针. 
    2. int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据 
    3. Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针 
    4. int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

    说到这里也就差不多了,我们的任务也就这么多,理解了这几个类型,其它的类型对我们来说也是小菜了,不过我们一般不会用太复杂的类型,那样会大大减小程序的可读性,请慎用,这上面的几种类型已经足够我们用了.

    一、细说指针

    指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。让我们分别说明。

    先声明几个指针放着做例子:
     

    例一:

    1. (1)int*ptr; 
    2. (2)char*ptr; 
    3. (3)int**ptr; 
    4. (4)int(*ptr)[3]; 
    5. (5)int*(*ptr)[4];


     

    1.指针的类型

    从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
    (1)int*ptr;//指针的类型是int*
    (2)char*ptr;//指针的类型是char*
    (3)int**ptr;//指针的类型是int**
    (4)int(*ptr)[3];//指针的类型是int(*)[3]
    (5)int*(*ptr)[4];//指针的类型是int*(*)[4]
    怎么样?找出指针的类型的方法是不是很简单?
     

    2.指针所指向的类型

    当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
    从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
    (1)int*ptr; //指针所指向的类型是int
    (2)char*ptr; //指针所指向的的类型是char
    (3)int**ptr; //指针所指向的的类型是int*
    (4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
    (5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]

    在指针的算术运算中,指针所指向的类型有很大的作用。
    指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。
     

    3.指针的值----或者叫指针所指向的内存区或地址

    指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
    以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
     

    4 指针本身所占据的内存区

    指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是否是左值时很有用。

     


     

    二、指针的算术运算

    指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。例如:
     

    例二:

    1.     char a[20]; 
    2.     int *ptr=(int *)a; //强制类型转换并不会改变a 的类型 
    3.     ptr++; 


     

    在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32 位程序中,是被加上了4,因为在32 位程序中,int 占4 个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:

    例三:

    1.     int array[20]={0}; 
    2.     int *ptr=array
    3.     for(i=0;i<20;i++) 
    4.     { 
    5.         (*ptr)++; 
    6.         ptr++ 
    7.     } 


     

    这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1 个单元,所以每次循环都能访问数组的下一个单元。

     

    再看例子:
     

    例四:

    1. char a[20]="You_are_a_girl"
    2. int *ptr=(int *)a; 
    3. ptr+=5;


     

    在这个例子中,ptr 被加上了5,编译器是这样处理的:将指针ptr 的值加上5 乘sizeof(int),在32 位程序中就是加上了5 乘4=20。由于地址的单位是字节,故现在的ptr 所指向的地址比起加5 后的ptr 所指向的地址来说,向高地址方向移动了20 个字节。
    在这个例子中,没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,ptr 是被减去5,那么处理过程大同小异,只不过ptr 的值是被减去5 乘sizeof(int),新的ptr 指向的地址将比原来的ptr 所指向的地址向低地址方向移动了20 个字节。
    下面请允许我再举一个例子:(一个误区)

    例五:

    1.     #include<stdio.h> 
    2.     int main() 
    3.     { 
    4.         char a[20]=" You_are_a_girl"
    5.         char *p=a; 
    6.         char **ptr=&p; 
    7.         //printf("p=%d\n",p); 
    8.         //printf("ptr=%d\n",ptr); 
    9.         //printf("*ptr=%d\n",*ptr); 
    10.         printf("**ptr=%c\n",**ptr); 
    11.         ptr++; 
    12.         //printf("ptr=%d\n",ptr); 
    13.         //printf("*ptr=%d\n",*ptr); 
    14.         printf("**ptr=%c\n",**ptr); 
    15.     } 


     

    误区一、输出答案为Y 和o
    误解:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char),所以输出如上结果,这个可能只是少部分人的结果.
    误区二、输出答案为Y 和a误解:ptr 指向的是一个char *类型,当执行ptr++;时,会使指针加一个sizeof(char *)(有可能会有人认为这个值为1,那就会得到误区一的答案,这个值应该是4,参考前面内容), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?答案是否定的.
    正解: ptr 的类型是char **,指向的类型是一个char *类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char*),即&p+4;那*(&p+4)指向哪呢,这个你去问上帝吧,或者他会告诉你在哪?所以最后的输出会是一个随机的值,或许是一个非法操作.

    总结一下:
    一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面,不多说了。
     

    三、运算符&和*

    这里&是取地址运算符,*是间接运算符。
    &a 的运算结果是一个指针,指针的类型是a 的类型加个*,指针所指向的类型是a 的类型,指针所指向的地址嘛,那就是a 的地址。
    *p 的运算结果就五花八门了。总之*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,它所占用的地址是p所指向的地址。
     

    例六:

    1. int a=12; int b; int *p; int **ptr; 
    2. p=&a; //&a 的结果是一个指针,类型是int*,指向的类型是 
    3. //int,指向的地址是a 的地址。 
    4. *p=24; //*p 的结果,在这里它的类型是int,它所占用的地址是 
    5. //p 所指向的地址,显然,*p 就是变量a 
    6. ptr=&p; //&p 的结果是个指针,该指针的类型是p 的类型加个* 
    7. //在这里是int **。该指针所指向的类型是p 的类型,这 
    8. //里是int*。该指针所指向的地址就是指针p 自己的地址。 
    9. *ptr=&b; //*ptr 是个指针,&b 的结果也是个指针,且这两个指针 
    10. //的类型和所指向的类型是一样的,所以用&b 来给*ptr  
    11. //值就是毫无问题的了。 
    12. **ptr=34; //*ptr 的结果是ptr 所指向的东西,在这里是一个指针, 
    13. //对这个指针再做一次*运算,结果是一个int 类型的变量。


     

    四、指针表达式

    一个表达式的结果如果是一个指针,那么这个表达式就叫指针表式。
    下面是一些指针表达式的例子:
    例七:
     

    1. int a,b; 
    2. int array[10]; 
    3. int *pa; 
    4. pa=&a; //&a 是一个指针表达式。 
    5. Int **ptr=&pa; //&pa 也是一个指针表达式。 
    6. *ptr=&b; //*ptr &b 都是指针表达式。 
    7. pa=array
    8. pa++; //这也是指针表达式。

    例八:

    1. char *arr[20]; 
    2. char **parr=arr; //如果把arr 看作指针的话,arr 也是指针表达式 
    3. char *str; 
    4. str=*parr; //*parr 是指针表达式 
    5. str=*(parr+1); //*(parr+1)是指针表达式 
    6. str=*(parr+2); //*(parr+2)是指针表达式

    由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。
    好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。在例七中,&a 不是一个左值,因为它还没有占据明确的内存。*ptr 是一个左值,因为*ptr 这个指针已经占据了内存,其实*ptr 就是指针pa,既然pa 已经在内存中有了自己的位置,那么*ptr 当然也有了自己的位置。
     

    五、数组和指针的关系

    数组的数组名其实可以看作一个指针。看下例:
     

    例九:

    1. int array[10]={0,1,2,3,4,5,6,7,8,9},value; 
    2. value=array[0]; //也可写成:value=*array; 
    3. value=array[3]; //也可写成:value=*(array+3); 
    4. value=array[4]; //也可写成:value=*(array+4);

    上例中,一般而言数组名array 代表数组本身,类型是int[10],但如果把array 看做指针的话,它指向数组的第0 个单元,类型是int* 所指向的类型是数组单元的类型即int。因此*array 等于0 就一点也不奇怪了。同理,array+3 是一个指向数组第3 个单元的指针,所以*(array+3)等于3。其它依此类推。
     

    例十:

    1. char *str[3]={ 
    2.     "Hello,thisisasample!"
    3.     "Hi,goodmorning."
    4.     "Helloworld" 
    5. }; 
    6. char s[80] 
    7. strcpy(s,str[0]); //也可写成strcpy(s,*str); 
    8. strcpy(s,str[1]); //也可写成strcpy(s,*(str+1)); 
    9. strcpy(s,str[2]); //也可写成strcpy(s,*(str+2));

    上例中,str 是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str 当作一个指针的话,它指向数组的第0 号单元,它的类型是char **,它指向的类型是char *。
    *str 也是一个指针,它的类型是char *,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变,且只能是右值.如果看成指针的话,他即是常量指针,也是指针常量.
    str+1 也是一个指针,它指向数组的第1 号单元,它的类型是char**,它指向的类型是char*。
    *(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,goodmorning."的第一个字符'H'

    下面总结一下数组的数组名(数组中储存的也是数组)的问题:
    声明了一个数组TYPE array[n],则数组名称array 就有了两重含义:
    第一,它代表整个数组,它的类型是TYPE[n];
    第二,它是一个常量指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0 号单元,该指针自己占有单独的内存区,注意它和数组第0 号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。在不同的表达式中数组名array 可以扮演不同的角色。在表达式sizeof(array)中,数组名array 代表数组本身,故这时sizeof 函数测出的是整个数组的大小。
    在表达式*array 中,array 扮演的是指针,因此这个表达式的结果就是数组第0 号单元的值。sizeof(*array)测出的是数组单元的大小。
    表达式array+n(其中n=0,1,2,.....)中,array 扮演的是指针,故array+n 的结果是一个指针,它的类型是TYPE *,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。在32 位程序中结果是4
     

    例十一:

    1. int array[10]; 
    2. int (*ptr)[10]; 
    3. ptr=&array;

    上例中ptr 是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array 代表数组本身。
    本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?
    答案是前者。例如:
    int(*ptr)[10];
    则在32 位程序中,有:
    sizeof(int(*)[10])==4
    sizeof(int[10])==40
    sizeof(ptr)==4
    实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。

     

    六、指针和结构类型的关系

    可以声明一个指向结构类型对象的指针。
     

    例十二:

    1.     struct MyStruct 
    2.     { 
    3.         int a; 
    4.         int b; 
    5.         int c; 
    6.     }; 
    7.     struct MyStruct ss={20,30,40}; 
    8.     //声明了结构对象ss,并把ss 的成员初始化为2030 40 
    9.     struct MyStruct *ptr=&ss
    10.     //声明了一个指向结构对象ss 的指针。它的类型是 
    11.     //MyStruct *,它指向的类型是MyStruct 
    12.     int *pstr=(int*)&ss; 
    13.     //声明了一个指向结构对象ss 的指针。但是pstr  
    14.     //它被指向的类型ptr 是不同的。 

    请问怎样通过指针ptr 来访问ss 的三个成员变量?
    答案:
    ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者
    ptr->b;
     

    ptr->c;

     

    又请问怎样通过指针pstr 来访问ss 的三个成员变量?
    答案:
    *pstr; //访问了ss 的成员a。
    *(pstr+1); //访问了ss 的成员b。
    *(pstr+2) //访问了ss 的成员c。
     

    虽然我在我的MSVC++6.0 上调式过上述代码,但是要知道,这样使用pstr 来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元: (将结构体换成数组)

     

    例十三:

    1. int array[3]={35,56,37}; 
    2. int *pa=array
    3. //通过指针pa 访问数组array 的三个单元的方法是: 
    4. *pa; //访问了第0 号单元 
    5. *(pa+1); //访问了第1 号单元 
    6. *(pa+2); //访问了第2 号单元

    从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。
    所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个"填充字节",这就导致各个成员之间可能会有若干个字节的空隙。
    所以,在例十二中,即使*pstr 访问到了结构对象ss 的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a 和成员b 之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。
    不过指针访问结构成员的正确方法应该是象例十二中使用指针ptr 的方法。



     

    七、指针和函数的关系

    可以把一个指针声明成为一个指向函数的指针。
    int fun1(char *,int);
    int (*pfun1)(char *,int);
    pfun1=fun1;
    int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。
    可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。
     

    例十四:

    1.     int fun(char *); 
    2.     inta; 
    3.     char str[]="abcdefghijklmn"
    4.     a=fun(str); 
    5.     int fun(char *s) 
    6.     { 
    7.         int num=0
    8.         for(int i=0;;) 
    9.         { 
    10.             num+=*s;s++; 
    11.         } 
    12.         return num; 
    13.     } 

    这个例子中的函数fun 统计一个字符串中各个字符的ASCII 码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s 后,实际是把str 的值传递给了s,s 所指向的地址就和str 所指向的地址一致,但是str 和s 各自占用各自的存储空间。在函数体内对s 进行自加1 运算,并不意味着同时对str 进行了自加1 运算。


     

    八、指针类型转换

    当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。
     

    例十五:

    1. float f=12.3
    2. float *fptr=&f; 
    3. int *p;

    在上面的例子中,假如我们想让指针p 指向实数f,应该怎么办?
    是用下面的语句吗?
    p=&f;
    不对。因为指针p 的类型是int *,它指向的类型是int。表达式&f 的结果是一个指针,指针的类型是float *,它指向的类型是float。
    两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0 上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行"强制类型转换":
    p=(int*)&f;
    如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP *TYPE, 那么语法格式是: (TYPE *)p;
    这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE *,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。
    而原来的指针p 的一切属性都没有被修改。(切记)
    一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,必须保证类型一致,否则需要强制转换
     

    例十六:

    1.     void fun(char*); 
    2.     int a=125,b; 
    3.     fun((char*)&a); 
    4.     void fun(char*s) 
    5.     { 
    6.         charc; 
    7.         c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; 
    8.         c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; 
    9.     } 

    注意这是一个32 位程序,故int 类型占了四个字节,char 类型占一个字节。函数fun 的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a 的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char *,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int *类型到char *类型的转换。
    结合这个例子,我们可以这样来
    想象编译器进行转换的过程:编译器先构造一个临时指针char *temp,然后执行temp=(char *)&a,最后再把temp 的值传递给s。所以最后的结果是:s 的类型是char *,它指向的类型是char,它指向的地址就是a 的首地址。
    我们已经知道,指针的值就是指针指向的地址,在32 位程序中,指针的值其实是一个32 位整数。
     

    那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:

    1.     unsigned int a; 
    2.     TYPE *ptr; //TYPE intchar 或结构类型等等类型。 
    3.     a=20345686
    4.     ptr=20345686; //我们的目的是要使指针ptr 指向地址20345686 
    5.       
    6.     ptr=a; //我们的目的是要使指针ptr 指向地址20345686 
    7.     //编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法: 
    8.     unsigned int a; 
    9.     TYPE *ptr; //TYPE intchar 或结构类型等等类型。 
    10.     a=N //N 必须代表一个合法的地址; 
    11.     ptr=(TYPE*)a //呵呵,这就可以了。 

    严格说来这里的(TYPE *)和指针类型转换中的(TYPE *)还不一样。这里的(TYPE*)的意思是把无符号整数a 的值当作一个地址来看待。上面强调了a 的值必须代表一个合法的地址,否则的话,在你使用ptr 的时候,就会出现非法操作错误。想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:
     

    例十七:

    1. int a=123,b; 
    2. int *ptr=&a; 
    3. char *str; 
    4. b=(int)ptr; //把指针ptr 的值当作一个整数取出来。 
    5. str=(char*)b; //把这个整数的值当作一个地址赋给指针str

    现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。



     

    九、指针的安全问题

    看下面的例子:
     

    例十八:

    1. char s='a'
    2. int *ptr; 
    3. ptr=(int *)&s; 
    4. *ptr=1298

    指针ptr 是一个int *类型的指针,它指向的类型是int。它指向的地址就是s 的首地址。在32 位程序中,s 占一个字节,int 类型占四个字节。最后一条语句不但改变了s 所占的一个字节,还把和s 相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。
    让我们再来看一例:
     

    例十九:

    1. char a; 
    2. int *ptr=&a; 
    3. ptr++; 
    4. *ptr=115;

    该例子完全可以通过编译,并能执行。但是看到没有?第3 句对指针ptr 进行自加1 运算后,ptr 指向了和整形变量a 相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。
    而第4 句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。
    在指针的强制类型转换:ptr1=(TYPE *)ptr2 中,如果sizeof(ptr2的类型)大于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2 的类型) 小于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2 所指向的存储区时是不安全的。至于为什么,读者结合例十八来想一想,应该会明白的。

     

     

     

    展开全文
  • C语言指针理解

    千次阅读 2018-11-24 21:21:45
    对于C语言初学者而言,指针部分有点复杂,也有些难理解,可以说是C语言学习之路上的第一道坎,在阅读了不少书籍资料和实验后,决定写下这篇博客,供大家参考。  什么是指针?  指针也是一种变量,存放的是某块...
  • 本文主要讲了一下关于C语言指针的相关概念,希望对你的学习有所帮助。
  • 对于C语言指针理解

    千次阅读 多人点赞 2019-02-17 17:57:35
    对于C语言指针理解 学习c语言中最难得部分就是指针了 指针是什么? 指针本身是一个变量,它存储的是数据在内存中的地址而不是数据本身的值。它的定义如下: 指针的形式: 类型 *变量名 比如int *p就是一个整型的...
  • 主要介绍了C语言指针及占据内存空间的相关知识,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
  • C语言指针变量的理解

    2020-02-10 22:29:37
    都说指针是C语言的灵魂,这只有正真理解掌握了...C语言指针是什么? 我们要知道计算机中的所有数据都必须放在内存中,不同类型的数据占用的字节数不一样; 例如 int 占用 4 个字节,char 占用 1 个字节。为了正...
  • 文章转自:无际单片机大家好,我是无际。今天给大家来讲解一下指针。我会由浅到深,最后结合实际应用讲解,让大家学会指针的同时,知道大佬们都用指针来干嘛!长文预警!全文大约5200多字,学指针看...
  • 从概念上理解,a 的分布像一个矩阵: 0 1 2 3 4 5 6 7 8 9 10 11 但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存: C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1]...
  • 关于C语言指针理解

    2020-07-21 16:44:07
    指针本质是地址,是内存单元的标号,是整型数。大小取决于系统虚拟内存的宽度,如:51系列的PC指针为16位,寻址范围16KB。
  • C语言 指针与数组的详解及对比 通俗理解数组指针指针数组 数组指针: eg:int( *arr)[10]; 数组指针通俗理解就是这个数组作为指针,指向某一个变量。 指针数组: eg:int*arr[10]; 指针数组简言之就是存放指针的数组...
  • C语言指针专题——如何理解指针

    千次阅读 多人点赞 2017-07-11 08:33:50
    指针的概念真的难以理解吗?主要是抓住本质!
  • C语言指针详解

    2019-01-16 16:45:51
    我们今天来攻克C 语言指针。 我并不打算使用过于官方、正统的语言来讲解指针。因为如果我这样做,就失去 了做这个教程的意义。如果需要,大家完全可以从各大教材、网站找到对指针正 ...程只适合新手理解指针
  • 当一维数组做函数参数的时候,会退化为指针 一维数组做函数参数的时候,c/c++编译器会做优化 int a[10] ----->int a[] ---->int *a 所以数组作函数参数的时候只能传指针 3.函数调用的时候,把数组首地址...
  • C语言指针讲解(史上最通俗最全面最经典)

    万次阅读 多人点赞 2019-04-10 21:44:15
    要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以...
  • 本课程的主要目的是通过介绍C语言指针指针变量、静态内存分配、动态内存分配以及结构体等知识,实现C语言到Java以及Python编程的一个完美过渡.
  • 深入理解C语言指针类型

    千次阅读 多人点赞 2018-05-24 15:01:46
    C语言指针类型
  • 详解C语言指针函数、函数指针、函数指针数组

    万次阅读 多人点赞 2018-07-09 11:08:24
    C语言中,指针是一个很重要但是又很容易弄错的概念,也可以说指针就是C语言的灵魂,所以说学好指针对于完全掌握C语言是一个必须的过程。而在指针中,指针函数、函数指针指针函数数组、函数指针数组、指向函数...
  • C语言指针知识点小结

    千次阅读 多人点赞 2020-01-10 15:40:06
      C语言指针基础知识点(一)–指针及指针变量   C语言指针基础知识点(二)–指针变量的引用   C语言指针基础知识点(三)–指针变量作为函数参数   C语言指针基础知识点(四)–通过指针引用数组   C语言指针...
  • 帮助理解指针
  • 本文主要介绍C语言指针作为函数返回值,这里整理了相关资料及示例代码,帮助大家学习理解此部分知识,有需要的同学可以参考下
  • C语言指针与汇编内存地址(二)

    千人学习 2016-05-26 10:37:35
    C语言指针与汇编内存地址视频教程,该课程学习和使用过C语言的程序员都认为要做到对指针这一概念的透彻理解和灵活运用相当困难。本课程会详细讲解C语言与指针、以及如何查看汇编内存等C语言高级教程内容。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 122,266
精华内容 48,906
关键字:

c语言指针的理解

c语言 订阅