↓
上期回顾: 【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 指针的解引用
【解引用基本语法】
数据类型 *指针变量名;
【解引用的步骤】
- 声明一个指针变量,并指定它将指向的数据类型。
- 初始化指针变量,使其指向一个有效的内存地址。
- 使用解引用操作符 *来访问指针所指向的内存地址中的值。
【解引用的示例】
//假设我们有一个整数变量 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
修饰变量的几个要点:
- 定义常量:在C和C++中,可以使用
const
关键字定义常量。例如:const int MAX_VALUE = 100; // 定义一个整数常量
- 局部变量:在函数内部,可以使用
const
来修饰局部变量,以表明该变量在函数执行过程中不会被修改。例如:void myFunction() { const int x = 10; // 定义一个局部常量 // x = 20; // 错误,常量不能被重新赋值 }
- 全局变量:在函数外部,可以使用
const
来修饰全局变量,以表明该变量在程序运行过程中不会被修改。例如:const int GLOBAL_CONSTANT = 100; // 定义一个全局常量
- 指针和引用:在C和C++中,
const
也可以用来修饰指针和引用,以表明指针或引用所指向的数据是不可变的。例如:const int *ptr; // 指针指向的数据是常量 const int &ref = num; // 引用所指向的数据是常量
使用 const
修饰变量时,需要确保在声明时已经初始化,因为常量在声明后不能被修改。此外,const
修饰的变量在内存中通常会有特殊处理,以保证其值不会被意外修改。
1.7.2 const修饰指针变量
const
关键字可以用来修饰指针变量,这有助于区分指针指向的数据是否可以被修改。const
修饰指针变量有几种不同的用法:
- 常量指针:当
const
修饰指针变量时,它表示指针本身是可变的,即指针可以被重新赋值以指向其他内存地址,但是指针所指向的数据不能被修改。例如:const int *ptr; // 指针指向的数据不可修改
- 指向常量的指针:当
const
放在指针变量后面时,它表示指针指向的数据是可变的,但是指针本身是不可变的,即指针不能被重新赋值以指向其他内存地址。例如:int *const ptr; // 指针本身不可变
- 指向常量的常量指针:当
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 = #
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
断言的一些要点:
- 定义:
assert
是一个宏,通常在C语言标准库中定义。在C中,它通常位于<assert.h>
头文件中。 - 使用:
assert
宏的基本语法如下:
这里,assert(表达式);
表达式
是一个条件表达式。如果表达式
的值为true
,程序继续执行;如果为false
,程序将调用预定义的错误处理函数,通常会输出错误信息并终止程序。 - 默认行为:如果
表达式
的值为false
,assert
宏会调用预定义的错误处理函数,通常包括打印错误信息并终止程序。在C中,默认行为是调用abort()
函数;在C++中,默认行为是调用std::abort()
函数。 - 自定义行为:在某些情况下,你可能希望自定义
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(); // 终止程序 }
- 调试与发布:在调试模式下,
assert
通常会被编译器包含在程序中,并且在断言失败时会抛出错误。在发布模式下,可以通过预处理器指令(如NDEBUG
)来禁用assert
,以提高程序的性能和减少不必要的错误消息。
总之,assert
是一个非常有用的工具,用于在程序运行时进行断言检查。它可以帮助开发者发现潜在的错误,并确保程序在预期条件下正常工作。它通常在调试模式下使用,以确保程序的稳定性和正确性。在发布模式下,可以通过预处理器指令来禁用assert
,以提高程序的性能和减少不必要的错误消息。
1.10.2 assert断言举例
在C语言中,assert
宏通常用于调试目的,以确保程序在特定条件下运行。以下是一些使用 assert
宏的例子:
- 检查输入参数:
#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; }
- 检查数组边界:
#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; }
- 检查动态内存分配:
#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