C语言(七)指针
::: warning :construction:WARNING
本条目仍在积极施工中,可能存在遗漏或不完善的地方
:::
指针
指针是 C 语言中一个非常强大且重要的概念。指针提供了直接访问内存地址的能力,可以用来操作数组、动态内存分配、函数参数传递等。
(一) 指针是什么?
指针是一个变量,与之前的变量不同,它保存了内存地址,而不是变量的值。指针变量的大小和普通变量一样,指针变量的值也是由操作系统分配的。指针变量的值是内存地址,而内存地址是 $8$ 字节。
1 | int i = 7; |
(二) 指针与函数
指针与函数之间是紧密相连的,指针可以作为函数的参数传递,函数也可以返回指针。
1 |
|
(三) 指针的赋值
我们前面提过,数组变量不能互相赋值,像下面这样的写法是不对的:
1 | int a[] = {1,2,3,4,5,6,7,8,9,10}; |
int b[] = a;是错误的写法
但是指向相同类型变量的指针变量是可以互相赋值的,如下面的代码:
1 |
|
:::warning
指向不同类型变量的指针不能互相赋值,如下面的代码:
1 |
|
:::
(四) 指向指针的指针
指针存储了它指向的变量的地址,指针的指针存储了它指向的指针的地址。
1 |
|
(五) const 修饰的指针
在C语言中,const
是一个关键字,用于声明常量。常量是在程序运行期间不可修改的值。
1 |
|
(六) 指针参与的运算
1 |
|
对指针进行大于、小于、大于等于、小于等于、等于、不等于这些运算符,都是用来比较两个指针变量的地址值(这么比除了看谁存储的位置靠前、谁靠后以外,好像没什么意义),并不是来比较指针指向的内存地址上的内容谁大谁小。
(七) *p 与 p 的区分
之前我们定义了一个指针变量:
1 | int *p; |
你可能对 *p
和 p
的区别有点迷糊,*p
是用来获取指针 p
所指向的内存地址上的内容。而 p
是一个名为 p
的指针变量,它存储的是一个地址。
(八) 指针的类型转换
指针类型转换是C语言中比较常见的操作,但是它并不是一个安全的操作,因为它可能会导致程序崩溃。
比如下面这个代码:
1 | void * p1;//一个不知道指向什么东西的指针 |
这个类型转换方式我们一般用不到,后面我们讲C语言的 qsort()
函数的时候可能会用到。
(九) 空指针
如果一个指针不指向任何数据,我们就称之为空指针 ,用 NULL
表示。例如:
1 | int *p = NULL; |
注意区分大小写,null
没有任何特殊含义,只是一个普通的标识符。NULL
是一个宏定义,在 stdio.h
被定义为:
1 |
宏我们后面会讲,这里只需要知道写了 #define 宏名 宏定义值
之后,编译器在编译时会自动把宏名替换成宏定义值。
1 | //比如 |
说完 NULL
,我们接着来说空指针:
1 | int *p = NULL; |
(十) 字符串
相信你已经看完了数组和指针的相关内容,我们来聊聊字符串。
1. 字符串是什么
字符串是以\0结尾的一串字符。\0标志符是字符串的结束标志,它告诉程序这个字符串结束了,程序在输出一个字符串的时候,看到\0就知道这个字符串到这里就结束了。
作为结束标志,\0并不会被输出到屏幕上,计算字符串长度的函数也不会把它算进去。
字符串在C语言中没有专门的数据类型,而是通过字符数组或字符指针来实现。
2. 字符数组的声明和定义
字符串实质上是一个字符数组,数组中每个元素都是字符,可以按照数组的方式访问字符串中的字符。
(1) 声明并初始化字符串
可以在声明时直接把一个字符串常量赋给它,这样编译器会自动在末尾添加空字符。
1 |
|
(2) 声明未初始化的字符数组
也可以声明一个未初始化的字符数组,然后逐个字符赋值,并手动添加空字符。
1 |
|
3. 使用字符指针声明字符串
1 |
|
1 | #include<stdio.h> |
上面展示了两种定义字符串的方法,第一种是数组,第二种是指针。虽然说数组名在表达式中会被转换为指向数组首元素的指针。但是这两种定义方式还是有区别的。
特性 | 字符数组 | 字符指针 |
---|---|---|
内存分配 | 编译器为数组分配足够的空间,包括终止符 | 指针指向只读存储区的字符串字面量 |
可修改性 | 可以修改数组内容 | 不可修改字符串字面量内容 |
存储位置 | 通常在栈上(函数内部)或全局数据区(全局作用域) | 指针在栈上,字符串字面量在只读存储区 |
4. 字符串赋值
(1). 直接赋值
下面这些写法都是正确的:
1 | char *t = "title"; |
(2). 通过scanf()函数读取
1 |
|
使用scanf读取字符串并写入数组时,不需要加取地址符号&,因为我们前面说过,数组名在表达式中会被转换为指向数组首元素的指针。
:::warning
误区警示:
1 | char *str1; |
:::
5. 常用的字符串函数
c语言提供了一些函数来操作字符串,它们都在<string.h>头文件中。
(1). strlen()函数
学到这里我们应该已经能看函数的定义和注释来知道它的用法的能力了。
strlen的原型声明如下:
1 | size_t strlen(const char *str); |
size_t 是C语言中用于表示大小的数据类型,通常被定义为 unsigned int 或 unsigned long,它的确切定义取决于具体的操作系统和编译器,这里不要管它。
参数列表中const char *str表示str是一个指向只读存储区的字符串常量的指针。也就是说传入的地址给了一个叫str的指针,这个指针只能读取它指向的区域的值,不能通过这个指针修改它指向的区域的值。
1 |
|
(2). strcmp()函数
strcmp函数用于比较两个字符串是否相等,它返回一个整数。
如果两个字符串相等,则返回0。
如果第一个字符串小于第二个字符串,则返回-1。
如果第一个字符串大于第二个字符串,则返回1。
这个大小指的是什么呢?指的并不是长度,而且两个数组从前往后对应的字符之间的大小关系。
谁大谁小是看字符的ASCII码值。
比如下面这俩数组
a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] |
---|---|---|---|---|---|---|---|
‘a’ | ‘b’ | ‘c’ | ‘d’ | ‘e’ | ‘f’ | ‘g’ | ‘\0’ |
b[0] | b[1] | b[2] | b[3] | b[4] |
---|---|---|---|---|
‘a’ | ‘b’ | ‘c’ | ‘d’ | ‘\0’ |
从前往后依次比较,a[0]和b[0]的ASCII码值相同,a[1]和b[1]的ASCII码值也相同,依次往后比,会发现a[4]和b[4]的ASCII码值不同,a[4]的ASCII码值大于b[4]的ASCII码值,所以返回一个正数1。
1 |
|
库函数也是程序员写的,我们也可以试试自己还原一个strcmp函数(感兴趣的同学可以试一下):
1 | /* |
(3). 其他常用函数
还有好多常用的字符串函数,这里我只给出原型声明和功能,大家可以自己尝试一下。
1 | char * strcpy(char *restrict dest, const char *restrict src); |
1 | char * strchr(const char *s, int c); |
1 | char* strrchr(const char *s, int c); |
1 | char * strstr(const char *s1, const char *s2); |
1 | char *strcasestr(const char *s1, const char *s2); |
6. 单字符输入输出
(1). putchar()函数
1 | //函数原型: |
(2). putchar()函数
1 | //函数原型: |
1 | //示例: |
(十一) 动态内存分配
在C语言中,动态内存分配是一种在运行时分配和管理内存的方式.
与静态内存分配(如通过数组等方式)不同,动态内存分配允许程序在需要时分配内存,并在不再需要时释放内存,以提高内存使用的灵活性和效率。
C语言提供了一组标准库函数,它们在<stdlib.h>头文件中。这些函数用于动态内存分配和管理,包括 malloc、calloc、realloc 和 free。
1. malloc()函数
malloc函数用于分配指定字节数的内存,返回一个指向分配内存的指针。如果分配失败,它返回 NULL。
1 | #include<stdio.h> |
如果剩余内存空间不够,malloc函数会返回NULL。
:::warning
常见误区:
分配的内存没有通过free函数释放,会导致内存泄漏,逐渐耗尽可用内存。
重复释放空间
使用已释放的内存
:::
2. realloc()函数
realloc函数用于重新分配内存,它接受一个指针作为参数,并返回一个指向重新分配内存的指针。如果重新分配失败,它返回 NULL。
一般需要重新扩展之前分配的内存块的大小的时候,可以使用realloc函数。
1 | //原型声明 |
注意:
realloc() 分配的新内存块可能与原来的不在同一个位置,因此不能使用原来的指针访问新的内存块。
建议在调用 realloc() 后立即检查返回值,以确保内存分配成功。
如果 realloc() 失败,原内存块不会被释放,需要手动释放。