【笔记】四小时彻底掌握C指针

1 篇文章 0 订阅
1 篇文章 0 订阅

前言

本文为4小时彻底掌握C指针的笔记。

P1 指针基本介绍

/*
前提条件:a的地址为204,p的地址为64
*/
#include "stdio.h"
int main(){
int a;
int *p;
p = &a;
a = 5; 

//p带星时为值
print p; // >> 204 a的内存地址
print &a;// >> 204 a的内存地址
print *p;// >> 5 a变量中存储的数据  // 此为解引用
print &p;// >> 64 p的内存地址

}

P2 指针代码示例

#include "stdio.h"
int main(){
    int n = 2;
    int* p;
    p = &n;
    printf("%p\n",p);
    printf("%p\n",p+1); //使用指针运算+1,指向了下一个该类型地址,int地址+4字节
    return 0;
}
OUTPUT:
0x7fffffffe28c //此处使用的是%p
0x7fffffffe290 //此处为+4字节后的地址

整数类型

下表列出了关于标准整数类型的存储大小和值范围的细节:

类型存储大小值范围
char1 byte-128 到 127 或 0 到 255
unsigned char1 byte0 到 255
signed char1 byte-128 到 127
int2 或 4 bytes-32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int2 或 4 bytes0 到 65,535 或 0 到 4,294,967,295
short2 bytes-32,768 到 32,767
unsigned short2 bytes0 到 65,535
long4 bytes-2,147,483,648 到 2,147,483,647
unsigned long4 bytes0 到 4,294,967,295

浮点类型

下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:

类型存储大小值范围精度
float4 byte1.2E-38 到 3.4E+386 位小数
double8 byte2.3E-308 到 1.7E+30815 位小数
long double10 byte3.4E-4932 到 1.1E+493219 位小数

不同系统间的差异

在这里插入图片描述

P3 指针的类型,算术运算,void指针

指针类型

指针属于强类型:需要用一个特定类型的指针变量来存放特定类型变量的地址。

#include "stdio.h"
int main(){
    int a = 1025;
    int *p;
    p = &a;
    printf("size of integer is %d bytes\n",sizeof(int));
    printf("Address = %d,value = %d\n",p,*p);
    char *p0;
    p0 = (char*)p;
    printf("size of char is %d bytes\n",sizeof(char));
    printf("Address = %d,value = %d\n",p0,*p0);
}
#OUTPUT:
size of integer is 4 bytes
Address = -13332,value = 1025
size of char is 1 bytes
Address = -13332,value = 1

上面的代码可以看到,将指针类型强制转换为char后,输出的value = 1,原因是 1025 的二进制形式为 0100 0000 0001,char为1字节,int为4字节,所以截取了后面的 0001,所以输出为 1。

void指针使用

void指针无法使用解引用*p0,也无法使用指针运算p0+1

#include "stdio.h"
int main(){
    int a = 1025;
    int *p;
    p = &a;
    void *p0;
    p0 = p; // 这里void指针是可以直接被赋值其他类型指针的
    printf("Address = %d",p0;
}

P4 指向指针的指针

#include "stdio.h"
int main(){
    int x = 5;
    int* p = &x;
    int** q = &p;
    int*** r = &q;
    printf("-------------\n");
    printf("    x = %d\n", x);
    printf("   &x = %d\n",&x);
    printf("-------------\n");
    printf("    p = %d\n",p); //指向x的地址
    printf("   *p = %d\n",*p);
    printf("   &p = %d\n",&p);
    printf("-------------\n");
    printf("    q = %d\n",q); //指向指针p的地址
    printf("   *q = %d\n",*q);//指向x的地址
    printf("  **q = %d\n",**q);//解引用x的值
    printf("-------------\n");
    printf("    r = %d\n",r);
    printf("   *r = %d\n",*r);
    printf("  **r = %d\n",**r);//指向x的地址
    printf(" ***r = %d\n",***r);//解引用x的值
    printf("-------------\n");
    printf("由此可见,没星号的时候输出为指针自身地址\n");
    printf("指针运算:r+3 = %d\n",r+3);
}
OUTPUT:
-------------
    x = 5
   &x = -13324
-------------
    p = -13324
   *p = 5
   &p = -13336
-------------
    q = -13336
   *q = -13324
  **q = 5
-------------
    r = -13344
   *r = -13336
  **r = -13324
 ***r = 5
-------------
由此可见,没星号的时候输出为指针自身地址
指针运算:r+3 = -13320

***r这么长的星号,俗称:解引用链

P5 函数传值 VS 传引用

指针作为函数参数使用,称为传引用。函数传值不会影响主函数中的变量值,传引用会影响主函数中的变量值。

在这里插入图片描述

Stack、Static/Global、Code(Text)这三个区段的大小在编译的时候就已经确定了,是固定的。

堆栈的方向

一个经常让人困惑的点在于,常见的文章中,堆栈经常是以相反的方向进行内存分配。如下图所示,Stack由高地址向低地址索取空间,Heap则相反从低向高。但是也有很别处是Stack自上而下,Heap相反。所以是不是写错了?到底哪种才是正确的分配方案。**其实根据系统架构的不同,这两种方式都是存在的。**只是顺序的颠倒,没有其它不同。

Bob想要实现通过一个外部函数调用来给主函数中的变量值+1,代码应该如何编写?

// 传引用代码示例
#include "stdio.h"
void Increment(int *p){
    *p = (*p) + 1;
}
int main(){
    int a;
    a = 10;
    Increment(&a);
    printf("a = %d\n",a);
}
//OUTPUT:
//a = 11

P6 指针和数组

假设存在数组 A[10],A的地址为A[0]的地址,此为数组的基地址。

#include "stdio.h"

int main(){
    int A[]={1,2,3,4,5};
    printf("%d\n",A);
    printf("%d\n",&A[0]);
    printf("%d\n",A[0]);
    printf("%d\n",*A);
}
OUTPUT:
-13344
-13344
1
1

P7 数组作为函数参数

#include "stdio.h"

int SumOfElement(int A[]){ //此处的int A[] 等同与 in* A
    int i, sum = 0;
    int size = sizeof(A)/sizeof(A[0]);
    printf("Function - Size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
    printf("Function arg size = %d\n",size);
    for(i = 0;i<size;i++){
        printf("A[%d]=%d\n",i,A[i]);
        sum+= A[i];
    }
    return sum;
}
int main(){
    int A[]={1,2,3,4,5};
    int total = SumOfElement(A); //A can‘t be used for &A[0]
    printf("Array SUM = %d\n",total);
    printf("Main - size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
}
Function - Size of A = 8, size of A[0] = 4
Function arg size = 2
A[0]=1
A[1]=2
Array SUM = 3
Main - size of A = 20, size of A[0] = 4

由上面的代码可知,外部函数中的size值为2,int size = sizeof(A)/sizeof(A[0]),这里的size是判断的外部函数中的数组长度,并不是Main中的数组长度。正确计算数组值的外部函数代码应为:

int SumOfElement(int A[]){
    int i, sum = 0;
    int size = sizeof(A)/sizeof(A[0]);
    printf("Function - Size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
    printf("Function arg size = %d\n",size);
    for(i = 0;i<5;i++){ //在这里设置判断的值为主函数中的数组长度
        printf("A[%d]=%d\n",i,A[i]);
        sum+= A[i];
    }
    return sum;
}

P8&P9 指针和字符数组

C里的字符串必须以NULL结尾,也就是\0结尾,假如存入数组的字符串为’John’,字符串长度为4,实际字符数组长度应为5,因为末尾要加上NULL。使用strlen()函数测量字符数组长度也是以NULL作为结尾,测量出的长度等同与字符数,不会额外计算NULL。

#include "stdio.h"

int main(){
    char c1[]="hello";
    char* c2;
    c2 = c1;
    printf("代码:char* c2;c2 = c1:\n");
    printf("-------------\n");
    printf("c1[0] :%c\n",c1[0]);
    printf("&c1[0]:%d\n",&c1[0]);
    printf("c2[0] :%c\n",c2[0]);
    printf("&c2[0]:%d\n",&c2[0]);
    printf("-------------\n");
    printf("c1:%d\n",c1);
    printf("c2:%d\n",c2);
    printf("-------------\n");
    printf("&c1:%d\n",&c1);
    printf("&c2:%d\n",&c2);
    printf("-------------\n");
    printf("TEST c3\n");   
    char c3[10];
    c3[0] = c1[0];
    printf("c3[0] :%c\n",c3[0]);
}
代码:char* c2;c2 = c1:
-------------
c1[0] :h
&c1[0]:-13318
c2[0] :h
&c2[0]:-13318
-------------
c1:-13318
c2:-13318
-------------
&c1:-13318
&c2:-13328
-------------
TEST c3
c3[0] :h

利用传引用NULL打印字符数组:

#include "stdio.h"

void print(char* c){
    int i = 0;
    while(c[i] != '\0'){
        printf("%c",c[i]);
        i++;
    }
}

int main(){
    char C[20] = "hello";
    print(C);
}

P10 指针和二维数组

指针的类型很重要,不是在你读地址的时候,而是再你解引用的时候,或者是你对它进行指针算术的时候。

假设存在int B[2][3]B实际上是一个指向包含三个整型数据的指针,也就是12字节(一个整型数据4字节)。

int* p = B    [x]
int(*p)[3] = B [√] 
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

指针数组和二维数组指针的区别

指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:

int *(p1[5]);  //指针数组,可以去掉括号直接写作 
int *p1[5];int (*p2)[5];  //二维数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。

int* p VS int(*p)[4]

int p:*

#include "stdio.h"

int main(){
    int a[3][4] = {  
    {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
    {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
    {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
    };
    int* p = a; //这里我能正确输出
    printf("a:%d\n",a);
    printf("a[1][1]:%d\n",a[1][1]);
    printf("p:%d\n",p);
    printf("*P:%d\n",*p);  //这里输出的是a[0][0]的值
}
OUTPUT:
a:-13376
a[1][1]:5
p:-13376
*P:0

二维数组在内存中的存储方式:

在这里插入图片描述

按照正确格式int(*p)[4]输入p:

#include "stdio.h"

int main(){
    int a[3][4] = {  
    {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
    {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
    {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
    };
    int(*p)[4] = a; //[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

    printf("a:%d\n",a);
    printf("a[1][1]:%d\n",a[1][1]);
    printf("p:%d\n",p);
    printf("*(p+1):%d\n",*(p+1)); // *(p+1) = p+1,指向第1行第0个元素的地址
    printf("**(p+1):%d\n",**(p+1)); // **(p+1) 指向第1行第0个元素的值 
    printf("*(*(p)+3):%d\n",*(*(p)+3)); // *(*(p)+3) 指向第0行第3个元素的值
}
OUTPUT:
a:-13376
a[1][1]:5
p:-13376
*(p+1):-13360
**(p+1):4
*(*(p)+3):3

int(*B)[4];

B[i][j] = *(B[i]+j) = *(*(B+i)+j) B[i]为地址,*(B+i)为地址

P11 指针和多维数组

                                                          ┌───┬───┬───┐
                                               ┌───┐  ┌──▶│ 1 │ 2 │ 3 │
                                           ┌──▶│░░░│──┘   └───┴───┴───┘
                                           │   ├───┤      ┌───┬───┬───┐
                                           │   │░░░│─────▶│ 4 │ 5 │ 6 │
                                           │   ├───┤      └───┴───┴───┘
                                           │   │░░░│──┐   ┌───┬───┬───┐
                                    ┌───┐  │   └───┘  └──▶│ 7 │ 8 │ 9 │
                            ns ────▶│░░░│──┘              └───┴───┴───┘
                                    ├───┤      ┌───┐      ┌───┬───┐
                                    │░░░│─────▶│░░░│─────▶│10 │11 │
                                    ├───┤      ├───┤      └───┴───┘
                                    │░░░│──┐   │░░░│──┐   ┌───┬───┐
                                    └───┘  │   └───┘  └──▶│12 │13 │
                                           │              └───┴───┘
                                           │   ┌───┐      ┌───┬───┬───┐
                                           └──▶│░░░│─────▶│14 │15 │16 │
                                               ├───┤      └───┴───┴───┘
                                               │░░░│──┐   ┌───┬───┐
                                               └───┘  └──▶│17 │18 │
                                                          └───┴───┘

如果我们要访问三维数组的某个元素,例如,ns[2][0][1],只需要顺着定位找到对应的最终元素15即可。

多维数组跟二维数组差不多,几维就几个星才能取到值。

方括号等于一星。

三维数组的指针传入:

voidFunc(int(*A)[2][2]){
    
}

P12 指针和动态内存 - 栈 VS 堆

堆:空闲的内存池,动态内存

静态内存是在栈上分配的,而动态内存是在堆上分配的。

为了在C中使用动态内存,我们需要知道4个函数:malloc calloc realloc free [ 这四个函数也可在C++中使用 ]

为了在C++中使用动态内存,我们需要知道4个函数:new delete

malloc

作用:从堆上找到空闲内存,为你预留空间然后通过void指针返回给你。如果无法找到空闲内存,malloc会返回NULL。

void指针一般被称为通用指针或叫泛指针。 它是C语言关于纯粹地址的一种约定。 当某个指针是void型指针时,所指向的对象不属于任何类型。 因为void指针不属于任何类型,则不可以对其进行算术运算,比如自增,编译器不知道其自增需要增加多少。

free

作用:使用malloc分配的内存,最终通过调用free进行释放。

int *p;
p = (int*)malloc(sizeof(int));
p = (int*)malloc(20*sizeof(int)); //分配数组
free(p);

new 和 delete

int *p;
p = new int;
delete p;
p = new int[20]; //分配数组
delete[] p;

P13 malloc,calloc,realloc,free

malloc

malloc - void* malloc(size_t size)

size_t是一种无符号的整型数,它的取值没有负数,在数组中也用不到负数,而它的取值范围是整型数的双倍。 sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。 该类型保证能容纳实现所建立的最大对象的字节大小。

calloc

calloc - void* calloc(size_t num,size_t size)

示例:

int* p = (int*)calloc(3,sizeof(int)) 

calloc返回void指针,但是calloc接收2个参数:

  1. 特定类型的元素的数量
  2. 类型的大小

malloc与calloc的区别:

  1. malloc 参数只有一个,calloc参数有两个
  2. malloc 分配完内存后不会对其进行初始化,因此如果没有填入值的话就会得到一些随机值(垃圾数据),calloc会对其进行初始化

realloc

realloc - void* realloc(void* ptr,size_t size)

realloc接收两个参数:

  1. 第一个参数是指向已分配内存的起始地址的指针
  2. 第二个参数是新的内存块的大小

realloc会把已分配内存块的内容拷贝过去 [ 感觉这个函数没啥用 ]

free

函数free的入参是内存的起始地址。

应用场景

假设用户想要输入一个变量n,并且创建一个数组为A[n],如何做?

  • 直接使用int A[n]: 会报错,因为静态内存需要明确指出数组的大小。
  • 正确做法:int* A = (int*)malloc(n*sizeof(int))

P14 指针和动态内存 - 内存泄漏

内存泄漏只会由于Heap引起。内存泄漏指的是我们动态申请了内存,但是即使是使用完了之后也从来都不去释放它。

栈的内存是自动回收的,栈帧结束后会自动释放,栈的大小是固定的。

P15 函数返回指针:return 地址

#include "stdio.h"
#include "stdlib.h"
void PrintHelloWorld(){
    printf("hello,world\n");
}
int* Add(int* a,int* b){
    int c = (*a) + (*b);
    return &c;
}
int main(){
    int a = 2, b = 4;
    int* ptr = Add(&a,&b);
    PrintHelloWorld();
    printf("Sum = %d\n",*ptr);
}

上面这个程序无法成功运行,你知道为什么吗?

因为函数返回指针时,该函数在栈里已经被释放掉了。

要使用函数返回指针,需要在heap中使用。因为heap的内存释放需要手动释放。

P16 函数指针

函数实际上是内存上的一块连续地址,函数指针指向起始地址。

#include "stdio.h"
#include "stdlib.h"

int Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int c;
    int (*p)(int, int); //函数指针
    p = &Add;
    c = (*p)(2, 3); //这行代码也可写成 c = p(2,3);
    printf("%d\n", c);
}

P17 函数指针的使用案例:回调函数

#include "stdio.h"
void A(){
printf("hello\n");
}
void B(void (*ptr)()){
    ptr();
}
int main(){
    void (*p)() = A;
    B(p);
}

其中,void (*p)() = A; B(p);可简写为B(A);,函数A为回调函数。

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值