c指针详细笔记

 课程链接: 

【强烈推荐】4小时彻底掌握C指针 - 顶尖程序员图文讲解 - UP主翻译校对 (已完结)_哔哩哔哩_bilibili

——————————————笔记大部分引用自:(大佬写的很详细)自用——————————(30条消息) C/C++ 指针详解_Adenialzz的博客-CSDN博客_c++指针详解  


     ——标识小标题

     ——标识关键词

     ——标识关键变量名


1、指针基本介绍

——指针的声明和引用

  • 指针的声明 在C语言中,我们通过 * 来声明一个指向某种数据类型的指针:int *p。这个声明的含义即:声明一个指针变量 p,它指向一个整型变量。换句话说,p 是一个可以存放整型变量的地址的变量。
  • 取地址 如果我们想指定 p 指向某一个具体的整型变量 a,我们可以:p = &a。其中用到了取地址运算符 &,它得到的是一个变量的地址,我们把这个地址赋值给 p,即使得 p 指向该地址。这时,如果我们打印p, &a, &p的值会得到什么呢?不难理解,应该分别是204,204,64。
  • 解引用 如果我们想得到一个指针变量所指向的地址存放的值,该怎么办呢?还是用 *放在指针变量 p 前面,即 *p 注意这里的 * 就不再是声明指针的意思了,而称为 解引用,即把 p 所指向的对象的值读出来。 所以如果我们打印 *p,则会得到其所指向的整型变量 a 的值:5。
  • 实际上,我们还可以通过解引用直接改变某个地址的值。比如 *p = 8,我们就将204地址处的整型变量的值赋为8。此时再打印*p或者a,则会得到8。(*p可直接打印)

关于*,&两个运算符的使用,可参考博客:指针(*)、取地址(&)、解引用(*)与引用(&)_Adenialzz的博客-CSDN博客_取地址和指针

——关于内存分配 

  • 内存(RAM)图中的每一个格子通常代表一个字节,每一个字节都有一个地址,在进行变量声明时,比如int a; 系统会给这个变量分配一些内存,分配多少取决于变量类型
  • int 4;char 1;float 4;其他的可以通过size of来查看
  • 当我们为变量赋值时,如a = 5,程序就会先查到 a 的类型及其首地址,然后到这个地址把其中存放的值写为 5。
  • 指针作为一种变量也需要占据一定的内存空间。由于指针的值是一个内存地址,所以指针所占据的内存空间的大小与其指向的数据类型无关,而与当前机器类型所能寻址的位数有关。具体来说,在32位的机器上,一个指针(指向任意类型)的大小为4个字节,在64位的机器上则为8个字节。

——图示

int类型变量,分配了204~207,值是以二进制的形式写入的,从起始地址204

2、指针代码示例

  •  就像变量可以同时声明和初始化:int a;a = 10;→int a = 10; 指针也可以这样操作,要格外注意:*p = 的是实际值;而int *p = 的是地址 即&变量
a = 10;
int *p;
p = &a;
转化为:
int *p = &a;
  • p + 1会得到下一个整型变量的地址,如果p是2002~2005 则p + 1是2006 p+1直接打印就行
printf("the value of p is %d",p);//2002
printf("the value of p+1 is %d",p+1);//2006

 3、指针类型、算数运算、void类型

——指针类型

  • 指针显示的是变量起始地址的值
  • 两个同类型指针之间可以赋值,类型不同则需要转化p0 =(char*)p;否则会发生编译错误

——算数运算

  • 指针唯一的算数运算就是以整数加减:p+1;p-2   
  • 对 p 执行加1,应该得到的是下一个整型数据(视指针类型而定)的地址,即在地址的数值上面应该加4。

——赋值、类型转换输出示例:    (地址无影响,值被类型影响)

指针之间的赋值就是把=右边指针里存的地址赋值给左边,此时两个指针变量都指向同一个变量,但是如果指针类型不同则需要强制转换,如(char*)p,  由于转成char,系统只取一个字节的值

危险

可能会造成危险的是,C/C++并不会为我们访问的地址进行检查,也就是说,我们可能通过指针访问一块未分配的内存(值是一些随机值),但是没有任何报错。这可能会造成我们不知不觉地弄错了一些数值。

 ——void类型

  • void *p0; 不指定指针指向类型,在使用时将其赋值为指向某种特定的数据类型的指针时不需要做强制类型转换。
  • 由于不知道它指向的类型,因此不能直接对其进行解引用*p,也不能对其进行算数运算p+1

4、指向指针的指针 

我们之所以能够把整型变量 x 的地址存入 p 是因为 p 是一个指向整型变量的指针int*。那如果想要把指针的地址也存储到一个变量中,这个变量就是一个指向指针的指针,即int**

#include <stdio.h>

int main(){
    int x;
    int* p = &x;
    *p = 6;
    int** q = &p;
    int*** r = &q;

    printf("%d\n", *p);
    printf("%d\n", *q);
    printf("%d\n", **q);//x的值
    printf("%d\n", **r);
    printf("%d\n", ***r);
    **q = *p + 2;
    printf("%d\n",**q);

    return 0;
}

在这里我们不按编译器实际输出的地址值来进行分析,因为这个地址值是不固定的且通常较大。笔者在这里画了一小段内存,我们按图中的地址值来分析打印输出的内容。在图中,红色字体是地址值,青色块是该变量占据的地址空间,其中的黑色字体是该变量的值。假设我们在32位机中,即一个指针占4个字节。

 注意,当*的数量变多时会晕掉。可以用钥匙开门的想法理解,*相当于钥匙,多个*相当于套间

5、函数传值 vs. 传引用

——内存四区

在执行一个C语言程序时,此程序将拥有唯一的“内存四区”——栈区、堆区、全局区、代码区.

具体过程为:操作系统把硬盘中的数据下载到内存,并将内存划分成四个区域,由操作系统找到main入口开始执行程序。

  • 堆区(heap):一般由程序员手动分配释放(动态内存申请与释放),若程序员不释放,程序结束时可能由操作系统回收。
  • 栈区(stack):由编译器自动分配释放,存放函数的形参、局部变量等。当函数执行完毕时自动释放。
  • 全局区(global / stack):用于存放全局变量和静态变量, 里面细分有一个常量区,一些常量存放在此。该区域是在程序结束后由操作系统释放。
  • 代码区(code / text):用于存放程序代码,字符串常量也存放于此。

——先了解一下全局变量局部变量:

全局变量:定义在函数外(所有函数也包括主函数)。

局部变量:定义在一个函数里的变量。

——函数传值(函数参数是整型变量)

如图,输出的a值依然是10,为什么?

#include <stdio.h>
void Increment(int a) {
  a = a + 1;
}
int main() {
   int a = 10;
   Increment(a);
   printf("a = %d",a);
}
这种叫传值调用,即把实参的值拷贝给了形参

在该程序中,Albert期望通过Increment()函数将a的值加1,然后打印出a = 11,但是,程序的实际运行结果却是a = 10。问题出在哪里呢?

实际上,这种函数调用的方式称为值传递call by value,这样在Increment()函数中,临时变量local variable a,会在该函数结束后立刻释放掉。也就是说Increment()函数中的a ,和main() 函数中的 a 并不是同一个变量

 在Increment()函数调用结束后,它的栈帧被释放掉,main()函数并不知道它做了什么main()自己的变量值一直是10,然后调用printf()函数,将该值打印出来。

 ——传引用(函数参数是指针)

那怎样才能实现Albert的预期呢?我们可以通过把指针作为函数参数来实现:

#include <stdio.h>

void Incremnet(int* p){  //这一步相当于int* p = &a
    *p = *p + 1;         //即便p被释放a的值已经改完了
}

int main(){
    int a;
    a = 10;
    Incremnet(&a);       //实参是地址
    printf("a = %d\n", a);

    return 0;
}

这种传地址的方式我们称之为传引用call by reference

它可以在原地直接修改传入的参数值。另外,由于传的参数是一个指针,无论被指向的对象有多么大,这个指针也只占4个字节(32位机),因此,这种方式也可以大大提高传参的效率

6、指针与数组简介

数组与指针的一个联系是:数组名就是指向数组首元素的指针。数组的首元素的地址,也被称为数组的基地址。

如果直接printf数组的名字比如A,会得到一个指向数组首元素的指针,解引用A会得到数组首元素的值,如下:

 int A[5] = {1,9,3,4,5};
    printf("%d\n",A);
    printf("%d\n",A+1);
    printf("%d\n",*A);
    printf("%d\n",*(A+1));
    int *p;
    p = A  //同类型指针之间可以赋值

前面提到过指针的算术运算可能会导致访问到未知的内存,因为我们定义一个指针时,它指向的位置的邻居通常是未知的。而在数组中,我们没有这个问题。因此在数组中,指针的算术运算就很有意义了。因为相邻位置的变量都是已知的,我们可以通过这种偏移量的方式去访问它们。 

想要得到数组里下标为i的地址有两种方法:&A[i]  和  (A + i)

想要得到数组里下标为i的有两种方法:  A[i]  和  *(A + i)

注意A++ 是非法操作,但可以int *p 然后p=A;p++ 是合法操作 

7、数组作为函数参数

sizeof会得到数组的字节数

sizeof(A)/sizeof(A[0])会得到数组元素个数

——数组作为函数参数示例一(注意size在for循环用到,作为单独参数输入)只能这种!

sum输出结果为15(32位机)

#include <stdio.h>

int SumOfElements(int A[], int size){
    int i, sum = 0;
    for (i=0; i<size; i++){
        sum += A[i];
    }
    return sum;
}


int main(){
    int A[] = {1, 2, 3, 4, 5};
    int size = sizeof(A) / sizeof(A[0]);
    int total = SumOfElements(A, size);
    printf("Sum of elements = %d\n", total);
  
    return 0;
}

——数组作为函数参数示例二(注意数组大小size不再作为函数参数单独输入) 错的

有人可能会想,既然我们已经将数组传入函数了,能不能进行进一步的封装,将数组元素个数的计算也放到调用函数内来进行呢?于是有了如下实现:

sum输出结果为1,为什么呢?

#include <stdio.h>

int SumOfElements(int A[]){
    int i, sum = 0;
    int size = sizeof(A) / sizeof(A[0]);
    
    for (i=0; i<size; i++){
        sum += A[i];
    }
    return sum;
}


int main(){
    int A[] = {1, 2, 3, 4, 5};
    int total = SumOfElements(A);
    printf("Sum of elements = %d\n", total);
  
    return 0;
}

为了测试问题出在哪里,让我们在main()SumOfElements()函数中打印如下信息:

printf("Main - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0]));
printf("SOE - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0]));			// 将这两行分别添加到main和SumOfElements

 

在被调函数中直接对数组名使用sizeof会返回指针的大小

分析

我们期望的是向左边那样,在main()函数将数组A作为参数传给SOE()之后,会在SOE()的栈帧上拷贝一份完全相同的20字节的数组。但是在实际上,编译器却并不是这么做的,而是只把数组A的首地址赋值给一个指针,作为SOE()的形参。也就是说,SOE()的函数签名SumOfElements(int A[]) 其实是相当于SumOfElements(int* A)。这也就解释了为什么我们在其内部计算A的大小时,得到的会是一个指针的大小。结合我们之前介绍过的值传递和地址传递的知识。可以这样讲:数组作为函数参数时是传引用(call by reference),而非我们预期的值传递。

需要指出的是,编译器的这种做法其实是合理的。因为通常来讲,数组会是一个很长,占内存空间很大的变量,如果每次传参都按照值传递完整地拷贝一份的话,效率极其低下。而如果采用传引用的方式,需要传递的只有一个指针的大小。
 

8、指针与数组辨析

这里视频里做了许多解释,原作者认为有一句话可以概括二者关系的本质:数组名称和指针变量的唯一区别是,不能改变数组名称指向的地址,即数组名称可视为一个指向数组首元素地址的指针常量也就是说数组名指针是定死在数组首元素地址的,其指向不能被改变。比如数组名不允许自加A++,因为这会它是一个不可改变的指针常量,而一般指针允许自加p++;还有常量不能被赋值,即若有数组名 A,指针 p,则A = p是非法的。详见博客:C++中数组和指针的关系(区别)详解_Adenialzz的博客-CSDN博客_c++数组与指针的区别

指针可以有数组的用法(下标)引用了这篇文章(2条消息) C语言--指针变量可以当作数组运用,指针转换为数组,指针当作数组使用,数组越界_沉不默的博客-CSDN博客_用指针代替数组 

来自mooc上的讨论 

8.1、指针与字符数组


格式字符用法(%c;%d)

%c 单个字符

%s 字符串(char)

%d 十进制整数(int)
 
%ld 十进制整数(long)
 
%f 十进制浮点数(float)
 
%lf 十进制浮点数(double)
 
%o 八进制数
 
%u 无符号十进制数(DWORD)
 
%x 十六进制数(0x00000)

后面的内容参见文章开头引用的笔记即可

——一些补充:

——指针常量与常量指针详解引用如下文章:

(3条消息) C语言中const关键字的用法_xingjiarong的博客-CSDN博客_const

——关于指针的写法总结:(3条消息) 让你不再害怕指针——C指针详解(经典,非常详细)_唐大麦的博客-CSDN博客_指针

int p; //这是一个普通的整型变量


int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针


int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组


int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组


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


int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.


int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据


Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针


int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

——关于二维数组:

在这里插入图片描述

#include <stdio.h>

int main(){
    int B[2][3] = {2, 3, 6, 4, 5, 8};
  
    printf("-----------------------\n");        // 指向一维数组的指针 	400
    printf("%d\n", B);                        //B是指向一维数组的指针,B[0]是指向整形元素的指针
    printf("%d\n", &B[0]);
    printf("-----------------------\n");        // 指向整型的指针 		  400
    printf("%d\n", *B);
    printf("%d\n", B[0]);
    printf("%d\n", &B[0][0]);
    printf("-----------------------\n");        // 指向一维数组的指针 	412
    printf("%d\n", B+1); 
    printf("%d\n", &B[1]);
    printf("-----------------------\n");        // 指向整型的指针     412
    printf("%d\n", *(B+1));
    printf("%d\n", B[1]);
    printf("%d\n", &B[1][0]);
    printf("-----------------------\n");        // 指向整型的指针    420 
    printf("%d\n", *(B+1)+2);
    printf("%d\n", B[1]+2); 
    printf("%d\n", &B[1][2]);             
    printf("-----------------------\n"); 				// 整型  3
    printf("%d\n", *(*B+1));                  
    printf("-----------------------\n"); 
    
  	return 0;
}

数组名类型的指针虽然存储首地址的值,但是位置也是定死的?看1和2的思维不能把B[0]当作指针来看,数组名虽然存储地址,但不代表完全是指针?

关于malloc函数的返回值为指针类型

(4条消息) C语言,返回指针的函数_Alienware^的博客-CSDN博客_c 返回指针

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值