C语言初阶指针详解

C语言函数详解

C语言数组详解

C语言操作符详解

C语言初阶指针详解

C语言结构体详解




初阶指针

  1. 指针是什么
  2. 指针和指针类型
  3. 野指针
  4. 指针运算
  5. 指针和数组
  6. 二级指针
  7. 指针数组

一、指针是什么

  • 🔼 指针是指内存中一个最小单元的编号,也就是地址

  • 🔼 口语中所说的指针,通常指的是指针变量(即用来存放内存地址的变量

  • 32位计算机(x86)中,内存会划分为一个个的内存单元(一个内存单元的大小为:1byte)

    • 32位计算机:x86

    • 64位计算机:x64

  • 每个内存单元都有一个编号

32位的电脑中,有32根地址线

00000000 00000000 00000000 00000000		-->0
00000000 00000000 00000000 00000001		-->1
00000000 00000000 00000000 00000010		-->2
00000000 00000000 00000000 00000011		-->3
... ...
11111111 11111111 11111111 11111111		-->4,294,967,295

2 32 = 4 , 294 , 967 , 295 ( B y t e ) 2^{32}=4,294,967,295(Byte) 232=4,294,967,295Byte 个地址序列
2 32 = 4 , 294 , 967 , 295 ( B y t e ) = 4 , 194 , 304 ( K b ) = 4096 ( M b ) = 4 ( G b ) \begin{aligned} 2^{32} &=4,294,967,295(Byte)\\ &=4,194,304(Kb)\\ &=4096(Mb)\\ &=4(Gb) \end{aligned} 232=4,294,967,295(Byte)=4,194,304(Kb)=4096(Mb)=4(Gb)

举例:

int main()
{
   int a = 11;//向内存申请4个字节,存储11

   printf("%p\n", &a);//&取地址操作符
   return 0;
}
  • 相较于64位的系统,在VS中监视变量a的存储过程

    0000000b

  • a存放的地址:

    请添加图片描述

  • 在内存中a的数据为

    0x00000005B9CFFBA4  0b 00 00 00  ....
                        ^^ ^^ ^^ ^^
                        || || || ||
                        A4 A5 A6 A7
    
  • 若要存储a的地址:

    int* p = &a;
    printf("%p\n", &a);//%p用于打印变量地址
    

    此时的p就是指针变量

    int* p = &a;
    
    int--->说明p指向的对象是int类型的
    *  --->说明p是指针变量
    
    • 内存单元

      编号—>地址—>地址也被称为指针

      因此存放指针(地址)的变量就是指针变量

      int main()
      {
      	int a = 11;
      	int* p = &a;
      	*p = 20;//解引用操作符,意思就是通过p中存放的地址,找到p所指向的对象,*p就是p指向的对象
      	printf("%d\n", a);//20
      	return 0;
      }
      
      • ⭐️*p = 20;
        • *p解引用操作符,意思就是通过p中存放的地址,找到p所指向的对象,*p就是p指向的对象(此处的p指向的对象是a)

指针变量的大小

//x64的环境下
int main()
{
   printf("%zd\n", sizeof(char*));		//8
   printf("%zd\n", sizeof(short*));		//8
   printf("%zd\n", sizeof(int*));		//8
   printf("%zd\n", sizeof(double*));	//8

   return 0;
}
  • 指针变量是用来存放地址的
  • 不管是什么类型的指针,都是在创建指针变量
  • 指针变量的大小取决于系统存放一个地址需要多大的空间
  • 32位(x86) 的系统上的地址:32bit - 4byte,所以此处指针变量的大小是4个字节
  • 64位(x64) 的系统上的地址:64bit - 8byte,所以此处指针变量的大小是8个字节

二、指针和指针类型

指针的解引用

指针类型:

  • int*
  • char*
  • float*
  • ··· ···
(Ⅰ)决定解引用时访问几个字节

举例:

1️⃣ int*的指针,解引用访问4个字节

int main() {
	int a = 0x11223344;//定义一个int型变量a

	return 0;
}
地址(&a)
0x0019FBB044 33 22 11
int main() {
	int a = 0x11223344;//定义一个int型变量a
int* pa = &a;//定义一个指针变量*pa来存储变量a的地址
	*pa = 0;//对地址进行解引用操作

	return 0;
}

*pa = 0;对地址进行解引用操作后:

地址(&a)
0x0019FBB000 00 00 00
  • 值由44 33 22 11变为00 00 00 00,改变了四个字节
  • 可见int*型的指针,解引用时访问4个字节

2️⃣ char*的指针,解引用访问1个字节

int main() {
	int a = 0x11223344;//定义一个int型变量a

	return 0;
}
地址(&a)
0x0019FBB044 33 22 11
int main() {
	int a = 0x11223344;//定义一个int型变量a
char* pa = (char*)&a;//定义一个指针变量*pa来存储变量a的地址
	*pa = 0;//对地址进行解引用操作

	return 0;
}

*pa = 0;对地址进行解引用操作后:

地址(&a)
0x0019FBB000 33 22 11
  • 值由44 33 22 11变为00 33 22 11,只改变一个字节
  • 因此char*型的指针,解引用时访问1个字节

结论:

  1. 指针类型决定了指针在解引用时访问几个字节
  2. int 类型的指针,解引用时访问4个字节
  3. char 类型的指针,解引用时访问1个字节
  4. double 类型的指针,解引用时访问8个字节
  5. ··· ···
(Ⅱ)决定指针的“步长”
int main() {
	int a = 0x11223344;
	int* pi = &a;
	char* pc = &a;

	printf("pi  =%p\n", pi);
	printf("pi+1=%p\n", pi+1);

	printf("pc  =%p\n", pc);
	printf("pc+1=%p\n", pc+1);

	return 0;
}
  •   pi  =0057FE24
      pi+1=0057FE28
      pc  =0057FE24
      pc+1=0057FE25
    

int型指针:

  • pipi+1之间相差四个字节,因此int型指针的步长为4
  • 0 x 0057 F E 28 − 0 X 0057 F E 24 = 4 b y t e 0x0057FE28-0X0057FE24=4byte 0x0057FE280X0057FE24=4byte

char型指针:

  • pcpc+1之间相差一个字节,因此int型指针的步长为1
  • 0 x 0057 F E 25 − 0 X 0057 F E 24 = 1 b y t e 0x0057FE25-0X0057FE24=1byte 0x0057FE250X0057FE24=1byte

请添加图片描述

  • 注意:

  • int main() {
    	int a = 0;
    	int* pi = &a;//int型指针
    	float* pf = &a;//float型指针
    	//*pi = 100;
    	//*pf = 100.0;
    	return 0;
    }
    
    • 虽然int型指针pifloat型指针pf解引用时访问的都为4个字节,且pi+1pf+1都是跳过4个字节,但由于int型变量与float型变量在内存中的存储方式有所差异,因此二者不能混为一谈

    • *pi = 100

      • 请添加图片描述
    • *pf = 100
      • 请添加图片描述

三、野指针

  • 野指针(Dangling Pointer)是一个未初始化的指针,或者是一个指向已经被释放或不再使用的内存的指针。由于C语言不提供自动内存管理,因此程序员需要手动管理内存分配和释放,这就增加了野指针出现的风险。

1、野指针出现的原因

(Ⅰ)未初始化的指针
int main(){
int *p;//局部指针变量未初始化,默认为随机值
	*p = 10;
return 0;
}

代码问题:

  • int *p; 声明了一个指针变量 p,但没有为它分配任何内存地址,也没有将其初始化为 NULL。这意味着 p 是一个野指针。
  • *p = 10; 这行代码尝试通过解引用 p 来写入值 10 到它所指向的内存地址。由于 p 没有指向有效的内存地址,这将导致未定义行为,可能会导致程序崩溃。
(Ⅱ)指针越界访问
int main() {
	int arr[10] = { 0 };
	int* p = arr;//-->arr[0]
	int i = 0;
	for (i = 0; i <= 10; i++) {
		*p = i;
		p++;
		//当指针指向的范围超出数组arr的范围时,p就是野指针
	}
	return 0;
}

代码问题:

  1. 数组越界:数组 arr 有10个元素,索引从0到9。在循环中,i 的范围是从0到10,这意味着当 i 等于10时,*p = i; 这行代码尝试写入数组的第11个元素,这是非法的。
  2. 指针越界:由于 p 是指向数组 arr 的指针,当 i 等于10时,p 将指向数组 arr 之后的内存地址,这可能导致野指针。

请添加图片描述

(Ⅲ)内存释放后未置空

当使用free()malloc()系列函数分配的内存被释放后,如果没有将指针设置为NULL,那么该指针就成为了野指针。

int *ptr = malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 10;
    free(ptr);
    // ptr 现在是野指针
    *ptr = 20; // 未定义行为,可能导致程序崩溃
}

free()malloc()是两个常用的内存管理函数,它们属于C标准库中的内存分配和释放函数。

  1. malloc():
    • malloc()函数用于动态分配内存。它接受一个参数,即需要分配的字节数,并返回一个指向分配内存的指针。如果分配失败,它会返回NULL
    • 语法:void *malloc(size_t size);
    • 示例:int *ptr = (int*)malloc(10 * sizeof(int)); 这行代码分配了10个整数大小的内存。
  2. free():
    • free()函数用于释放之前通过malloc()calloc()realloc()分配的内存。调用free()后,这块内存将被操作系统回收,可以被后续的内存分配请求重新使用。
    • 语法:void free(void *ptr);
    • 示例:free(ptr); 这行代码释放了之前通过malloc()分配的内存。

注意事项

  • 在使用free()释放内存后,应该将指向该内存的指针设置为NULL,以避免产生野指针。野指针指的是指向已经释放或未初始化的内存的指针,使用野指针可能会导致程序崩溃或数据损坏。

例如:

int *ptr = (int*)malloc(10 * sizeof(int));
if (ptr != NULL) {
    // 使用内存
    free(ptr);
    ptr = NULL;  // 将指针置空,避免野指针
}
(Ⅳ)返回局部变量的地址
int tx() {
	int a = 10;
	return &a;
}

int main() {
	int* p = tx();//tx()中变量a的生命周期结束
  //但a所在的地址中依然还存放着10,直到该地址被其它内容覆盖
	return 0;
}

问题分析:

  1. 返回局部变量的地址:函数 tx 中定义了一个局部变量 a,并返回了它的地址。当函数 tx 返回时,局部变量 a生命周期结束,其占用的内存可能会被其他程序或函数重用。
  2. 野指针:在 main 函数中,p 指向了 tx 返回的地址。由于 a 的生命周期已经结束,p 现在指向了一个不确定的内存地址,这使得 p 成为一个野指针。

2、如何避免野指针

  1. 初始化指针:确保所有指针在使用前都被初始化。
  2. 释放内存后置空:释放内存后,将指针设置为NULL。
  3. 小心指针越界
  4. 使用静态或全局变量:如果需要在函数外部访问数据,考虑使用静态或全局变量。
  5. 使用引用计数:对于共享数据,可以使用引用计数来管理内存的生命周期。
  6. 避免返回指向局部变量的指针:不要返回指向函数内部局部变量的指针。

四、指针运算

1、指针加减整数

  • 当你对指针进行加法运算时,指针的值会增加其数据类型的字节大小乘以加数

  • 例如,对于 int 类型的指针,指针增加1将会使指针移动到下一个 int 的地址。

  •   int arr[5] = {0, 1, 2, 3, 4};
      int *p = arr;
      p = p + 1; // p 现在指向 arr[1]
    
  • 与加法相反,指针减法会减少指针的值其数据类型的字节大小乘以减数。

  •   int *p = arr + 4; // p 现在指向 arr[4]
      p = p - 2; // p 现在指向 arr[2]
    

例1:

int main() {
	int arr[5];
	int* p;
	//将数组中的内容全部初始化为1
	for (p = &arr; p < &arr[5];) {
		*p = 1;
		p++;
	}
	return 0;
}

例2:

int *p = arr;
p += 3; // p 现在指向 arr[3]
p -= 1; // p 现在指向 arr[2]

例3:指针偏移

  • 可以使用指针和整数的乘法或除法来实现指针的偏移。

  •   p = p + 2; // 指针向前移动两个 int 的大小
      p = p - 2; // 指针向后移动两个 int 的大小
      p = p + i; // 指针向前移动 i 个 int 的大小
      p = p - i; // 指针向后移动 i 个 int 的大小
    

3、指针相减

指针相减得到的是指针之间的元素个数(区分正负)

相减的指针必须指向同一片空间

int main() {
	int arr[10] = { 0 };
	printf("&arr[9] - &arr[0] = %d\n", &arr[9] - &arr[0]);
	printf("&arr[0] - &arr[9] = %d\n", &arr[0] - &arr[9]);
	return 0;
}
  •   &arr[9] - &arr[0] = 9
      &arr[0] - &arr[9] = -9
    

4、指针的关系运算

指针比较

  • 可以比较两个指针是否相等或不等,或者比较它们指向的地址的前后关系。

  •   if (p == q) {
          // p 和 q 指向相同的地址
      }
      if (p < q) {
          // p 指向的地址在 q 指向的地址之前
      }
    
#define dat 5
int main() {
	int arr[dat];
	int* p;

	return 0;
}

  •   	for (p = &arr[dat]; p > &arr[0];) {
      		--p;
      		*p = 0;
      	}
    
    • &arr[dat]>&arr[0]允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较
    • 请添加图片描述
  •   	for(p = &arr[dat - 1]; p >= &arr[0];vp--){
      		*vp = 0;
      	}
    
    • &arr[dat - 1]>=&arr[0]指向数组元素的指针与指向第一个元素之前的那个内存位置的指针进行比较(不符合标准
    • 请添加图片描述

五、指针和数组

  1. 数组名作为指针
  • 数组名在大多数情况下被用作指向数组第一个元素的指针。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;  // p 指向数组的第一个元素
  1. 访问数组元素
  • 可以通过指针运算或数组索引来访问数组元素:
int value1 = arr[2];  // 使用数组索引
int value2 = *(arr + 2);  // 使用指针运算
int value3 = *(p + 2);  // 使用指针运算,p 是指向数组首元素的指针

arr[i]–>*(arr+i)

  1. 指针算术与数组索引
  • 指针算术和数组索引在访问数组元素时是等价的:
int value = arr[index];  // 数组索引
int *p = arr;
value = *(p + index);  // 指针算术
  1. 遍历数组
  • 可以使用指针来遍历数组
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *p);
    p++;
}
  1. 多维数组
  • 对于多维数组,指针可以用于简化访问元素的过程
int arr[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

int* p = &arr[0][0];  // p 指向二维数组的第一个元素
for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", *(p + i * 3 + j));
    }
}
  1. 数组作为函数参数
  • 数组作为函数参数时,数组名会被转换为指向数组第一个元素的指针:
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}
  1. 指针数组
  • 指针数组是存储指针的数组,每个指针可以指向不同的数据:
int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
    printf("%d ", **(arr + i));
}
  1. 字符串和字符数组
  • 字符数组在C语言中经常被用作字符串:
char str[] = "Hello, World!";
char *p = str;
while (*p) {
    putchar(*p);
    p++;
}

六、二级指针

二级指针,也称为双重指针指针的指针,在C语言中是一个指向指针的指针。它通常用于多级间接访问数据,或者用于函数参数,以便函数可以修改指针所指向的地址。

示例:

int main() {
	int a = 10;
	int*  pa = &a;	//一级指针变量,指向变量a的地址
	int** ppa = &pa;//二级指针变量,指向变量pa的地址

	*pa = 20;
	printf("a=%d\n", a); 
	**ppa = 30;
	printf("a=%d\n", a);
	return 0;
}
  •   a=20
      a=30
    

请添加图片描述

  • 二级指针变量ppa一次解引用得到的是一级指针变量pa的内容,即:*ppa=pa
  • 二级指针变量ppa二次解引用得到的才是变量a的内容,即:**ppa=a

七、指针数组

即: 存放指针的数组

int main() {
	int a = 1;
	int b = 2;
	int c = 3;

	int* parr[10] = { &a,&b,&c };
	//parr是存放指针的数组(指针数组)

	int i = 0;
	for (i = 0; i < 3; i++) {
		printf("%d ", *(parr[i]));
	}
	return 0;
}
  •   1 2 3
    

这里,parr 是一个数组,包含10个元素,每个元素都是一个指向 int 类型的指针。

请添加图片描述

示例:利用指针数组模拟二维数组

int main() {
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,6 };

	int* parr[3] = { arr1,arr2,arr3 };

	//打印
	int i = 0;
	for (i = 0; i < 3; i++) {
		int j = 0;
		for (j = 0; j < 4; j++) {
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}
  •   1 2 3 4
      2 3 4 5
      3 4 5 6
    

printf("%d ", parr[i][j]); —>parr[i]等价于*(parr+i),因此不需要解引用

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值