【C语言回顾】指针(超详细!!一篇文章搞定!!!)

在这里插入图片描述
在这里插入图片描述


上期回顾: 【C语言回顾】操作符详解
个人主页:C_GUIQU
专栏:【C语言学习】

在这里插入图片描述

前言

各位小伙伴大家好!
今日天有晴,阳光灿烂;今日地有情,花团锦簇;今日海有情,浪迭千重;今日人有情,欢聚一堂!
指针是C语言的灵魂,同时也是大多数人的噩梦。不懂指针,相当于没学C语言。
上期小编给大家讲解了C语言中的操作符,接下来我们好好地细说指针!

在这里插入图片描述

1. 深入理解指针(一)

1.1 指针概括

【指针概念】指针是一个变量,它的值是一个内存地址。

当我们说一个指针指向一个变量时,实际上是指它存储了该变量的内存地址。

  • 指针的类型:指针有特定的类型,这决定了它可以指向的数据类型。

例如,int* 是一个指针类型,它可以指向一个整数类型的变量。

  • 指针的声明:指针需要声明其类型。

例如,int* ptr; 声明了一个整数指针变量 ptr。

  • 指针的初始化:指针在使用前需要被初始化,即给它一个有效的内存地址。

这通常通过取变量地址来实现,例如int num = 10; int* ptr = & num;。

  • 指针的解引用:指针的解引用是指通过指针访问它所指向的变量。

例如,*ptr 表示指针 ptr 所指向的变量。

  • 指针的算术运算:指针可以进行算术运算,以移动其在内存中的位置。

例如,ptr += 1 会将 ptr 移动到下一个内存地址。

  • 指针与数组:指针可以用来访问数组中的元素。

例如,int arr[10]; int* ptr = arr; 之后,ptr[3] 将会访问数组 arr 的第四个元素。

  • 指针与函数:指针可以作为函数的参数传递。这允许函数修改调用者提供的内存地址所指向的数据。

  • 指针与动态内存分配:指针可以用来分配和释放动态内存,例如使用 malloc、calloc、realloc 和 free 函数。

1.2 内存和地址

“内存”通常指的是存储器,它用来存储程序和数据。
“地址”是指向内存中特定位置的指针。
【理解】内存单元的编号 = 地址 = 指针

在计算机中我们把内存单元的编号称为地址。C语⾔中给地址起了一个新的名字:指针

在这里插入图片描述

1.3 指针变量和地址

1.3.1 指针变量

  • 指针变量是一种特殊类型的变量,它存储的是内存地址,而不是数据本身。
  • 指针变量的值就是它所指向的数据在内存中的地址。
  • 指针变量的类型决定了它能够指向的数据类型。例如,int* 是一个指针类型,它可以指向一个整数类型的变量。

1.3.2 地址

  • 地址是指向内存中特定位置的指针。
  • 每个变量在内存中都有一个唯一的地址,这个地址是该变量在内存中的位置标识。
  • 通过指针,程序可以访问和修改内存中的数据,因为指针提供了数据在内存中的位置信息。

1.3.3 指针变量和地址的关系

  • 指针变量存储的值是内存地址。
  • 指针变量的类型决定了它能够指向的数据类型。
  • 通过指针变量,程序可以间接地访问它所指向的内存地址中的数据。

1.4 解引用操作符

解引用操作符通常表示为 *
【用法】

  • 声明一个指针变量。例:int* ptr;
  • 初始化指针,使其指向一个变量。例:int num = 10; ptr = & num;
  • 使用解引用操作符获取指针所指向的值。例:int value = *ptr; 此时 value 将被设置为 num 的值。

1.5 指针变量的大小

在大多数现代计算机系统中,指针的大小取决于指针所指向的数据类型。指针本身通常占用的空间与操作系统和编译器有关,但通常情况下,指针的大小是固定的,并且与指针所指向的数据类型无关。
在64位操作系统中,指针的大小通常是8字节(64位),而在32位操作系统中,指针的大小通常是4字节(32位)。这意味着,无论指针指向什么类型的数据,指针本身的大小都是固定的。
例如,假设有一个整数指针 int* ptr;,在64位系统中,ptr的大小是8字节,无论它指向什么整数类型的变量。同样,在32位系统中,ptr 的大小是4字节。
需要注意的是,指针的大小与指针所指向的数据类型的大小是不同的。例如,一个整数指针 int* ptr; 指向一个整数 int num = 10;,在这个例子中,num 的大小取决于它存储的整数的大小,例如在大多数系统上,整数通常是4字节。而指针 ptr的大小,如前所述,取决于操作系统和编译器。

【结论】
• 32位平台下地址是32个bit位,指针变量大小是4个字节。
• 64位平台下地址是64个bit位,指针变量大小是8个字节。
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

1.6 指针变量类型的意义

1.6.1 指针的解引用

【解引用基本语法】

数据类型 *指针变量名;

【解引用的步骤】

  1. 声明一个指针变量,并指定它将指向的数据类型。
  2. 初始化指针变量,使其指向一个有效的内存地址。
  3. 使用解引用操作符 *来访问指针所指向的内存地址中的值。

【解引用的示例】

//假设我们有一个整数变量 int num = 10;,我们想要通过一个指针来访问这个变量的值。
int num = 10; // 声明并初始化整数变量
int *ptr; // 声明一个整数指针变量

ptr = # // 初始化指针变量,使其指向 num 的内存地址

// 解引用指针变量,访问 num 的值
int value = *ptr; // value 将被设置为 num 的值
//在这个例子中,*ptr 表示解引用操作,它访问了指针 ptr 所指向的内存地址中的值,并将这个值赋给了变量 value。

1.6.2 指针的简单加减

  • 指针加整数:当指针加上一个整数时,它指向的内存地址会增加或减少相应数量的内存单元。例如,如果 ptr 是一个指向整数的指针,那么 ptr += 1 会将 ptr 移动到下一个整数的地址。
  • 指针减整数:当指针减去一个整数时,它指向的内存地址会减少或增加相应数量的内存单元。例如,如果 ptr 是一个指向整数的指针,那么 ptr -= 1 会将 ptr 移动到前一个整数的地址。
  • 指针加减指针:指针还可以进行加减运算,这通常用于数组操作。例如,如果 ptr1 和 ptr2 都是指向数组中元素的指针,那么 ptr1 += 2 会将 ptr1 移动到数组中第三个元素的地址,而 ptr1 -= ptr2 会将 ptr1 移动到 ptr2 所指向的元素的下一个元素的地址。
  • 指针加减指针类型:指针还可以进行加减运算,其中一个是整数类型,另一个是指针类型。这通常用于数组操作和动态内存分配。例如,如果 ptr1是一个指向数组的指针,那么 ptr1 += 2 会将 ptr1 移动到数组中第三个元素的地址。

1.6.3 void* 指针

在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引⽤的运算。
【示例】

#include <stdio.h>

int main()
{
   
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	return 0;
}

【使用void*类型的指针接收地址】

#include <stdio.h>
int main()
{
   
	int a = 10;
	void* pa = &a;
	void* pc = &a;

	*pa = 10;
	*pc = 0;
	return 0;
}

1.7 const 修饰指针

1.7.1 const修饰变量

const 关键字可以用来修饰变量,以表明该变量是不可变的,即其值在程序运行过程中不能被改变。使用 const
修饰变量可以提高代码的清晰度和可维护性,因为它明确地告诉程序员该变量是一个常量。

以下是关于 const 修饰变量的几个要点:

  1. 定义常量:在C和C++中,可以使用 const 关键字定义常量。例如:
    const int MAX_VALUE = 100; // 定义一个整数常量
    
  2. 局部变量:在函数内部,可以使用 const 来修饰局部变量,以表明该变量在函数执行过程中不会被修改。例如:
    void myFunction() 
    {
         
        const int x = 10; // 定义一个局部常量
        // x = 20; // 错误,常量不能被重新赋值
    }
    
  3. 全局变量:在函数外部,可以使用 const 来修饰全局变量,以表明该变量在程序运行过程中不会被修改。例如:
    const int GLOBAL_CONSTANT = 100; // 定义一个全局常量
    
  4. 指针和引用:在C和C++中,const 也可以用来修饰指针和引用,以表明指针或引用所指向的数据是不可变的。例如:
    const int *ptr; // 指针指向的数据是常量
    const int &ref = num; // 引用所指向的数据是常量
    

使用 const 修饰变量时,需要确保在声明时已经初始化,因为常量在声明后不能被修改。此外,const 修饰的变量在内存中通常会有特殊处理,以保证其值不会被意外修改。

1.7.2 const修饰指针变量

const 关键字可以用来修饰指针变量,这有助于区分指针指向的数据是否可以被修改。const 修饰指针变量有几种不同的用法:

  1. 常量指针:当 const 修饰指针变量时,它表示指针本身是可变的,即指针可以被重新赋值以指向其他内存地址,但是指针所指向的数据不能被修改。例如:
    const int *ptr; // 指针指向的数据不可修改
    
  2. 指向常量的指针:当 const 放在指针变量后面时,它表示指针指向的数据是可变的,但是指针本身是不可变的,即指针不能被重新赋值以指向其他内存地址。例如:
    int *const ptr; // 指针本身不可变
    
  3. 指向常量的常量指针:当 const 关键字同时放在指针变量前面和后面时,表示指针本身和它所指向的数据都是不可变的。这意味着指针不能被重新赋值,也不能修改它所指向的数据。例如:
    const int *const ptr; // 指针本身和指向的数据都是不可变的
    

使用 const 修饰指针变量时,需要根据实际需要选择合适的修饰方式,以确保指针的行为符合预期,并且能够有效地保护数据不被意外修改。

1.8 指针运算

【分类】
• 指针± 整数
• 指针-指针
• 指针的关系运算

1.8.1 指针 + 整数

#include <stdio.h>

int main()
{
   
	int arr[10] ={
    1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr)/sizeof(arr[0]);
	for(i=0; i<sz; i++)
	{
   
		printf("%d ",*(p+i));//p+i 这⾥就是指针+整数
	}
	return 0;
}

1.8.2 指针 - 指针

#include <stdio.h>

int my_strlen(char *s)
{
   
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;
}
int main()
{
   
 printf("%d\n", my_strlen("abc"));
 return 0;
}

1.8.3 指针的关系运算

#include <stdio.h>

int main()
{
   
	int arr[10] ={
    1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr)/sizeof(arr[0]);
	while(p<arr+sz) //指针的⼤⼩⽐较
	{
   
		printf("%d ",*p);
		p++;
	}
	return 0;
}

1.9 野指针

【概念】野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1.9.1 野指针成因

【指针未初始化】

#include <stdio.h>
int main()
{
    
	 int *p;//局部变量指针未初始化,默认为随机值
	 *p = 20;
	 return 0;
}

【指针越界访问】

#include <stdio.h>
int main()
{
   
	 int arr[10] = {
   0};
	 int *p = &arr[0];
	 int i = 0;
	 for(i=0; i<=11; i++)
	 {
   
	 //当指针指向的范围超出数组arr的范围时,p就是野指针
	 *(p++) = i;
	 }
	 return 0;
}

【指针指向的空间释放】

#include <stdio.h>
int* test()
{
   
	int n = 100;
	return &n;
}

int main()
{
   
	int* p = test();
	printf("%d\n",*p);
	return 0;
}

1.9.2 如何规避野指针

1.9.2.1 指针初始化
#include <stdio.h>
int main()
{
   
	 int num = 10;
	 int*p1 = &num;
	 int*p2 = NULL; 
	 return 0;
}
1.9.2.2 小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。

1.9.2.3 指针变量不再使⽤时,及时置NULL,指针使用之前检查有效性
#include<stdio.h>

int main()
{
   
	int arr[10] ={
    1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	for(i=0; i<10; i++)
	{
   
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤
	//...
	p = &arr[0];//重新让p获得地址
	if(p != NULL) //判断
	{
   
		//...
	}
	return 0;
}

1.10 assert断言

1.10.1 assert断言理解

在C语言中,assert 是一个宏,用于在程序运行时进行断言检查。断言是一种条件表达式,它在程序执行时被评估。如果断言表达式的值为 true,程序继续执行;如果为 false,程序会停止执行并抛出一个错误。
以下是关于 assert 断言的一些要点:

  1. 定义assert 是一个宏,通常在C语言标准库中定义。在C中,它通常位于 <assert.h> 头文件中。
  2. 使用assert 宏的基本语法如下:
    assert(表达式);
    
    这里,表达式 是一个条件表达式。如果 表达式 的值为 true,程序继续执行;如果为 false,程序将调用预定义的错误处理函数,通常会输出错误信息并终止程序。
  3. 默认行为:如果 表达式 的值为 falseassert 宏会调用预定义的错误处理函数,通常包括打印错误信息并终止程序。在C中,默认行为是调用 abort() 函数;在C++中,默认行为是调用 std::abort() 函数。
  4. 自定义行为:在某些情况下,你可能希望自定义 assert 的行为。这可以通过定义一个 assert 函数来实现,该函数在 assert 宏被调用时被调用。例如:
    void my_assert(const char *expr, const char *file, int line) {
         
        fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", expr, file, line);
        abort(); // 终止程序
    }
    
  5. 调试与发布:在调试模式下,assert 通常会被编译器包含在程序中,并且在断言失败时会抛出错误。在发布模式下,可以通过预处理器指令(如 NDEBUG)来禁用 assert,以提高程序的性能和减少不必要的错误消息。
    总之,assert 是一个非常有用的工具,用于在程序运行时进行断言检查。它可以帮助开发者发现潜在的错误,并确保程序在预期条件下正常工作。它通常在调试模式下使用,以确保程序的稳定性和正确性。在发布模式下,可以通过预处理器指令来禁用 assert,以提高程序的性能和减少不必要的错误消息。

1.10.2 assert断言举例

在C语言中,assert 宏通常用于调试目的,以确保程序在特定条件下运行。以下是一些使用 assert 宏的例子:

  1. 检查输入参数
    #include <stdio.h>
    #include <assert.h>
    int add(int a, int b) {
         
        assert(a >= 0 && b >= 0); // 确保参数非负
        return a + b;
    }
    int main() {
         
        int result = add(-1, 2);
        printf("Result: %d\n", result);
        return 0;
    }
    
  2. 检查数组边界
    #include <stdio.h>
    #include <assert.h>
    void printArray(int arr[], int size) {
         
        assert(size > 0); // 确保数组不为空
        for (int i = 0; i < size; i++) {
         
            printf("%d ", arr[i]);
        }
        printf("\n");
    }
    int main() {
         
        int arr[] = {
         1, 2, 3, 4, 5};
        printArray(arr, 0); // 断言失败,因为数组为空
        return 0;
    }
    
  3. 检查动态内存分配
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    int* createArray(int size) {
         
        int *ptr = malloc(size * sizeof(int));
        assert(ptr != NULL); // 确保动态内存分配成功
        return ptr;
    }
    int main() {
         
        int *arr = createArray(0); // 断言失败,因为尝试分配零大小
        return 0;
    }
    

在这些例子中,assert 宏用于检查某些条件是否满足,这些条件通常是程序运行的基础假设。如果这些条件不满足,assert 宏将抛出一个错误,并终止程序。这有助于在开发过程中及时发现潜在的问题,并确保程序在实际运行时不会遇到未定义的行为。在发布版本中,可以通过预处理器指令 NDEBUG 来禁用 assert 宏,以提高程序的性能。

1.11 指针的使用和传址调用

1.11.1 strlen的模拟实现

strlen 函数是C语言标准库中的一个函数,用于计算字符串的长度,不包括字符串末尾的 \0 字符。在某些情况下,如果标准库不可用或者需要更深入理解字符串处理,可以模拟实现 strlen 函数。
以下是 strlen 函数的模拟实现:

#include <stdio.h>
int my_strlen(const char *s)
{
   
    int length = 0;
    while (*s++) {
   
        length++;
    }
    return length;
}
int main()
{
   
    char str[] = "Hello, World!";
    printf("The length of the string is: %d\n", my_strlen(str));
    return 0;
}

在这个实现中,我们定义了一个名为 my_strlen 的函数,它接受一个指向字符串的指针 s。函数内部使用一个循环来遍历字符串中的每个字符,并使用 length 变量来计数。循环继续直到遇到字符串末尾的 \0 字符,此时循环结束,length 变量中存储的就是字符串的长度。
需要注意的是,my_strlen 函数是按值传递字符串的,这意味着它不会修改原始字符串。如果需要修改原始字符串,可以使用指针来传递字符串。此外,my_strlen 函数返回的是字符串的长度,不包括 \0 字符。

1.11.2 传值调用和传址调用

在C语言中,函数参数的传递方式主要有两种:传值调用(Cal

评论 192
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值