一、什么是指针
- 指针表示内存地址(如酒店的房间号表示房间的地址)
- 我们平时说的指针,是保存地址的指针变量
二、定义指针变量
【1】格式
数据类型 *指针变量名;
*前面如果有数据类型,表示定义指针变量
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a,b,c;
printf("%p\t%p\t%p\n",&a,&b,&c);
//使用指针变量保存a的地址
int *p = &a;
printf("p=%p\n",p);
//因为指针变量p的值就是内存地址,所以使用%p格式符打印
return 0;
}
【2】指针的初始化和赋值
指针和变量的关系
指针指向一个变量实际上是指向该变量的首地址
#include <stdio.h>
int main(int argc, const char *argv[])
{
int *p; //定义了一个能够保存一个int类型变量地址的指针p
//上面的p现在是一个野指针(不知道指向的指针)
int a=90;
//用int类型变量a的地址给指针p赋值
//让指针p指向变量a
p = &a;
/*******************通过指针访问变量的值***********************/
//拿到变量的地址后,需要对地址解引用访问到地址中的内容
//可以使用(*地址)解引用
printf("%d\n",*p);
*p = 12; //<==>a=12
printf("a=%d\n",a);
//定义指针p1并初始化
int *p1 = NULL; //NULL表示空地址,当指针没有明确指向时可以指向NULL
return 0;
}
【3】通过指针的间接访问
- 直接通过变量名访问变量是直接访问
- 通过变量的地址(指针)访问变量是间接访问
- 如何通过指针访问变量:对地址进行解引用(对地址取*)
- *p;char *p1;
总结*的用法:
1、定义时,表示定义了指针变量
2、解引用时,表示取地址中的内容
如果*前面有数据类型一定是定义指针变量,
如果*前面没有数据类型一定是对地址的解引用
#include <stdio.h>
int main(int argc, const char *argv[])
{
int *p; //定义了一个能够保存一个int类型变量地址的指针p
//上面的p现在是一个野指针(不知道指向的指针)
int a=90;
//用int类型变量a的地址给指针p赋值
//让指针p指向变量a
p = &a;
//定义指针p1并初始化
int *p1 = NULL; //NULL表示空地址,当指针没有明确指向时可以指向NULL
/*******************通过指针访问变量的值***********************/
//拿到变量的地址后,需要对地址解引用访问到地址中的内容
//可以使用(*地址)解引用
printf("%d\n",*p);
*p = 12; //<==>a=12
printf("a=%d\n",a);
return 0;
}
【4】指针的运算
- 因为指针是一个内存地址,对于指针的大部分运算没有实际意义。
- 指针常用的操作是加法和减法
#include <stdio.h>
int main(int argc, const char *argv[])
{
int *p; //定义了一个能够保存一个int类型变量地址的指针p
//上面的p现在是一个野指针(不知道指向的指针)
int a=90;
//用int类型变量a的地址给指针p赋值
//让指针p指向变量a
p = &a;
printf("p=%p\n",p); //0x3c
printf("p+1=%p\n",p+1); //0x40,相差四个字节 p+1表示p所指向空间的下一个位置
printf("++p=%p\n",++p); //++p表示指针变量p本身向后偏移一个字节
char c;
char *p1=&c;
printf("p1=%p\n",p1); //0x73
printf("p1+1=%p\n",p1+1); //0x74,相差四个字节
return 0;
}
指针的加减运算,表示向后或者向前偏移n个字节。
n由指针的数据类型决定。
【5】指针的大小
- 指针的大小和数据类型无关,指针的数据类型只决定指针的偏移量
- 指针的大小由操作系统决定,32位操作系统占4Byte,64位操作系统占8Byte。
【6】指针和一维整形数组
- 数组的特点:数组名表示数组的首地址
- 所以指针指向一维数组,可以直接将数组名赋值给指针
[i]对地址先偏移i个字节,再解引用(取*)
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个包含 5 个元素的一维整型数组,并初始化元素值
int arr[5]={12,8,59,61,70};
// 定义指针 p 指向数组 arr 的首地址
int *p=arr;
// 输出数组 arr 的首地址
printf("%p\n",arr);
// 输出数组 arr 偏移 3 个元素后的地址
printf("%p\n",arr+3);
// 注释掉的代码,若执行会导致越界访问,因为数组只有 5 个元素
//printf("%p\n",p+5);
// 输出指针 p 偏移 1 个元素后对应的值,即 arr[1]
printf("%d\n",*(p+1));
// 先取指针 p 所指向的值(即 arr[0]),再加上 2 后输出
printf("%d\n",*p+2);
// p[3] 等价于 *(p + 3),输出数组中第 4 个元素的值
printf("%d\n",p[3]);
return 0;
}
【7】指针和一维整形数组的关系及运算
#include <stdio.h>
// 主函数入口
int main(int argc, const char *argv[])
{
// 定义一个包含4个整数的数组arr
int arr[4] = {1, 9, 34, 2};
// 定义一个整型指针p,并将其指向数组arr的首地址
// 数组名arr本身就是数组首元素的地址
int *p = arr;
// 也可以写成 p = &arr[0]; 两者效果相同
// 输出指针p指向的数组元素,即arr[0]
printf("%d", *p); // *p 等同于 arr[0]
/*
* 注释说明指针p与数组arr的关系及运算:
* p 可以直接当作数组名arr来使用,表示数组的首地址
* arr[i] 等价于 *(p + i),即通过指针偏移i个元素来访问数组元素
* p[i] 是 *(p + i) 的简写形式,也等同于 arr[i]
* *(p + i) 和 *(arr + i) 是等价的,都表示访问数组中偏移i个元素后的值
* p + i 和 arr + i 都表示指向数组中第i个元素的地址
* [i]操作符可以理解为先对指针进行i个元素的偏移,然后解引用获取该位置的值
*/
// 输出指针p偏移1个元素后的地址及该地址处的值(即arr[1])
printf("p+1=%p\t*(p+1)=%d
", p + 1, *(p + 1)); // 等同于 arr[1]
// 输出数组名arr偏移2个元素后的地址及该地址处的值(即arr[2])
printf("arr+2=%p\t*(arr+2)=%d
", arr + 2, *(arr + 2)); // 等同于 arr[2]
// 输出指针p偏移3个元素后的地址及该地址处的值(即arr[3])
printf("p+3=%p\t*(p+3)=%d
", p + 3, *(p + 3)); // 等同于 arr[3]
return 0;
}
测试题:
- 用指针的形式,完成对数组中元素的输入和输出
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个包含 5 个整数的数组 arr
int arr[5];
// 定义一个整型指针 p,并将其指向数组 arr 的首地址
// 这样指针 p 就可以用来操作数组 arr 中的元素
int *p = arr;
// 定义一个整型变量 i,用于循环计数
int i;
// 第一个 for 循环,用于从标准输入读取 5 个整数到数组 arr 中
// sizeof(arr) 表示数组 arr 占用的总字节数
// sizeof(arr[0]) 表示数组中一个元素占用的字节数
// sizeof(arr)/sizeof(arr[0]) 计算出数组 arr 的元素个数,这里为 5
for(i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
{
// 使用 scanf 函数从标准输入读取一个整数
// p + i 表示数组 arr 中第 i 个元素的地址
// 因为 scanf 需要传入变量的地址来存储输入的值
scanf("%d", p + i);
// 注释说明数组下标和指针偏移的等价关系
// [i] 操作符本质上等同于 *(指针 + i),即先偏移再解引用
// &*(p + i) 中,& 和 * 是互逆操作,会相互抵消,所以 &*(p + i) 就等同于 p + i
//[i]<==>*(+i)
//&*(p+i) &和*互相抵消
}
// 第二个 for 循环,用于将数组 arr 中的元素依次输出到标准输出
for(i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
{
// 使用 printf 函数输出数组元素的值
// *(p + i) 表示访问指针 p 偏移 i 个位置后所指向的元素的值
// \t 是制表符,用于在输出中添加间隔,使输出更整齐
printf("%d\t", *(p + i));
}
// 程序正常结束,返回 0 表示成功
return 0;
}
- 通过指针,求数组中元素的最大值
#include <stdio.h>
// 主函数,程序的入口点
int main(int argc, const char *argv[])
{
// 定义一个包含 4 个整数的数组 arr,并进行初始化
int arr[4] = {12, 50, 39, 71};
// 初始化变量 max 为数组的第一个元素,用于存储数组中的最大值
int max = arr[0];
// 定义一个整型指针 p,将其指向数组 arr 的首地址
// 这样指针 p 就可以用来遍历数组 arr 中的元素
int *p = arr;
// 注释说明指针 p 和数组名 arr 是等价的,都代表数组的首地址
//p<==>arr
// 定义一个整型变量 i,用于循环计数
int i;
// 使用 for 循环遍历数组中的每一个元素
for(i = 0; i < 4; i++)
{
// 比较当前最大值 max 和指针 p 偏移 i 个位置后所指向的元素的值
// *(p + i) 表示访问数组中第 i 个元素的值
if(max < *(p + i))
{
// 如果当前元素的值大于 max,则更新 max 的值为该元素的值
max = *(p + i);
}
}
// 输出数组中的最大值
printf("最大值为%d\n", max);
// 程序正常结束,返回 0 表示成功
return 0;
}
- 定义一个数组,用指针指向该数组并用指针的形式完成冒泡排序。
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个包含 7 个整数的数组 arr 并初始化
int arr[7] = {12, 71, 28, 34, 59, 20, 7};
// 定义一个整型指针 p,使其指向数组 arr 的首地址
// 后续可通过指针 p 来访问数组 arr 中的元素
int *p = arr;
// 定义循环控制变量 i 和 j,以及用于交换元素值的临时变量 temp
int i, j, temp;
// 计算数组 arr 的长度
// sizeof(arr) 得到数组占用的总字节数,sizeof(arr[0]) 得到数组单个元素占用的字节数
// 两者相除得到数组元素的个数
int len = sizeof(arr) / sizeof(arr[0]);
// 外层循环,控制冒泡排序的轮数,总共需要进行 len - 1 轮
for (i = 1; i < len; i++)
{
// 内层循环,每一轮比较相邻元素并交换位置
// 每一轮比较的次数会随着轮数增加而减少,因为每一轮都会将最大的元素放到正确位置
for (j = 0; j < len - i; j++)
{
// 通过指针对地址解引用的方式访问数组中元素
// 比较相邻两个元素的大小,如果前一个元素大于后一个元素
if (*(p + j) > *(p + j + 1))
{
// 交换两个元素的值
// 先将前一个元素的值保存到临时变量 temp 中
temp = *(p + j);
// 把后一个元素的值赋给前一个元素
*(p + j) = *(p + j + 1);
// 再把临时变量 temp 中保存的值赋给后一个元素
*(p + j + 1) = temp;
}
}
}
// 遍历排序好的数组并输出每个元素
for (i = 0; i < len; i++)
{
// 通过指针偏移和解引用的方式访问数组元素并输出
printf("%d\n", *(p + i));
}
// 程序正常结束,返回 0
return 0;
}
- 使用指针,实现数组的逆置
/*
*代码实现了数组的反转。通过指针运算和双指针法(一个指向开头,一个指向末尾),逐步交换对应位置的*元素,直到两个指针相遇或交错,从而完成数组的反转。
*/
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个包含7个整数的数组arr
int arr[7] = {12, 71, 28, 34, 59, 20, 7};
// 定义一个整型指针p,并将其指向数组arr的首地址
int *p = arr; // 指针p指向数组arr
int i, j, temp;
// 计算数组的长度
int len = sizeof(arr) / sizeof(arr[0]);
// 初始化两个指针,i指向数组开头,j指向数组末尾
i = 0;
j = len - 1;
// 当i小于j时,进行循环交换元素
while (i < j)
{
// 保存p指向的当前i位置的值
temp = *(p + i);
// 将j位置的值赋给i位置
*(p + i) = *(p + j);
// 将保存的i位置的值赋给j位置
*(p + j) = temp;
// i向后移动一位
i++;
// j向前移动一位
j--;
}
// 遍历并打印反转后的数组元素
for (i = 0; i < len; i++)
{
printf("%d", *(p + i));
}
return 0;
}
/*
*同样实现了数组的反转。与方法一不同的是,这里直接使用了指针i和j来进行操作,更加直观地体现了指针*的移动和元素的交换。通过不断移动指针并交换对应位置的元素,最终实现数组的反转。
*这两种方法都利用了指针的特性来操作数组,提高了代码的灵活性和效率。您可以根据实际需求选择适合的方法
*/
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个包含7个整数的数组arr
int arr[7] = {12, 71, 28, 34, 59, 20, 7};
// 定义一个整型指针p,并将其指向数组arr的首地址
int *p = arr; // 指针p指向数组arr
// 定义指针i指向数组的第一个元素
int *i = arr;
// 计算数组的长度
int len = sizeof(arr) / sizeof(arr[0]);
int temp;
// 定义指针j指向数组的最后一个元素
int *j = arr + len - 1;
// 当i指针小于j指针时,进行循环交换元素
while (i < j)
{
// 交换i和j指向的元素的值
temp = *i;
*i = *j;
*j = temp;
// i指针向后移动一位
i++;
// j指针向前移动一位
j--;
}
return 0;
}
【8】指针和一维字符数组
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个字符数组str并初始化为"hello",剩余部分自动填充为空字符'\0'
char str[100] = "hello";
// 定义一个字符指针p,并将其指向字符数组str的首地址
char *p = str;
/*
* 注释及解析:
* 对于字符数组str和字符指针p,以下关系成立:
* str[i] <==> p[i]
* 这是因为str作为数组名,在表达式中会被解释为指向数组首元素的指针,
* 所以str[i]和p[i]都是访问数组中第i个元素的方式。
* *(str + i) <==> *(p + i)
* 这里利用了指针算术,str + i表示从str指向的地址开始偏移i个字符的位置,
* *(str + i)就是解引用该地址得到第i个字符;同理对于p + i和*(p + i)。
* str + i <==> p + i
* str和p都表示数组首元素的地址,所以str + i和p + i都表示指向数组中第i个元素的地址。
* 这些地址在数值上是相等的,只是类型(指向字符的指针)相同。
*/
// 下面是一些示例用法:
printf("%c", str[1]); // 输出'h',等同于p[1]或*(str + 1)或*(p + 1)
printf("%c", *(str + 4)); // 输出'o',等同于*(p + 4)
return 0;
}
/*
在C语言中,数组名在大多数表达式中会被解释为指向数组首元素的指针。因此,对于一维字符数组str和字符指针p(指向str的首地址),它们在很多情况下可以互换使用。
str[i]和p[i]都是访问数组中第i个元素的合法方式。
(str + i)和*(p + i)通过指针算术实现了相同的功能,即访问数组中第i个元素。
str + i和p + i都表示指向数组中第i个元素的地址。
这些关系本质上与指针指向一维整型数组时的一致,只是数据类型从整型变成了字符型。理解这些基本概念对于掌握C语言中的指针和数组操作至关重要。
*/
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个字符数组str,并初始化为字符串"hello"
// 剩余的95个字符会自动初始化为空字符'\0'
char str[100] = "hello";
// 定义一个字符指针p,并将其指向字符数组str的首地址
char *p = str;
// 打印数组名str的值,即数组首元素的地址
// 在C语言中,数组名在大多数表达式中会被解释为指向数组首元素的指针
printf("str=%p", str);
// 打印数组名str偏移1个字符后的地址
// 即指向数组中第二个元素的地址
printf("str+1=%p", str + 1);
// 打印指针p的值,它指向数组str的首地址,与str相同
printf("p=%p", p);
// 打印指针p偏移1个字符后的地址
// 即指向数组中第二个元素的地址,与str+1相同
printf("p+1=%p", p + 1);
return 0;
}
/*
需要了解及注意
1. 数组名与指针的关系:
数组名在大多数表达式中会被解释为指向数组首元素的指针。因此,str和p在这里都表示指向字符数组str首元素的地址。
2. 指针算术:
str + 1和p + 1都表示指向数组中第二个元素的地址。因为str和p都是指向字符的指针,所以+1操作会使指针偏移一个字符的大小(通常是1字节)。
3. 打印地址:
使用%p格式说明符来打印指针的值(即地址)。注意,输出的地址值会根据程序的运行环境和编译器而有所不同。
4. 数组初始化:
char str[100] = "hello";这行代码不仅初始化了数组的前6个字符(包括结尾的空字符\0),还自动将剩余的95个字符初始化为空字符\0。
/
测试题:
- 终端输入字符串,如果有大写字母转为小写字母,如果是小写字母转为大写字母,其他字符转为#
解题思路
1.字符串输入:
- 使用
gets(str);
读取用户输入的一行字符串到str
数组中。 - 注意:
gets
函数是不安全的,因为它不检查缓冲区溢出。建议使用fgets
函数代替,如注释中所示。
2.字符串遍历与处理:
- 使用
while (*(str + i))
循环遍历字符串,直到遇到空字符\0
。 - 在循环中,通过指针算术
*(str + i)
访问字符串中的每个字符。
3.字符转换:
- 如果当前字符是大写字母(ASCII码在65到90之间),通过加上32将其转换为小写字母。
- 如果当前字符是小写字母(ASCII码在97到122之间),通过减去32将其转换为大写字母。
- 如果当前字符既不是大写字母也不是小写字母,将其替换为
#
字符。
4.字符串输出:
- 使用
puts(str);
输出处理后的字符串。
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个字符数组str,用于存储输入的字符串
char str[100];
// 使用gets函数读取一行输入到str中
// 注意:gets函数是不安全的,因为它不检查缓冲区溢出,建议使用fgets代替
gets(str);
// 更安全的替代方案:
// fgets(str, sizeof(str), stdin);
// int i = 0; // 这行代码被注释掉了,但它在原代码中用于遍历字符串
// 使用while循环遍历字符串,直到遇到空字符'\0'
while (*(str + i)) // 如果没有走到'\0'继续循环
{
// 如果当前字符是大写字母(ASCII码在65到90之间)
if (*(str + i) >= 'A' && *(str + i) <= 'Z')
{
// 将其转换为小写字母,方法是加上32(大小写字母间的ASCII码差值)
*(str + i) += 32;
}
// 如果当前字符是小写字母(ASCII码在97到122之间)
else if (*(str + i) >= 'a' && *(str + i) <= 'z')
{
// 将其转换为大写字母,方法是减去32
*(str + i) -= 32;
}
// 如果当前字符既不是大写字母也不是小写字母
else
{
// 将其替换为'#'字符
*(str + i) = '#';
}
// 移动到下一个字符
i++;
}
// 使用puts函数输出处理后的字符串
puts(str);
return 0;
}
- 使用指针实现字符串函数族的函数:strlen、strcpy、strcat、strcmp
看好了,开始实现了呦
#include <stdio.h>
// 自定义 strlen 函数,计算字符串长度
size_t my_strlen(const char *str)
{
const char *p = str;
while (*p)
{
p++;
}
return p - str;
}
// 自定义 strcpy 函数,复制字符串
char *my_strcpy(char *dest, const char *src)
{
char *p = dest;
while (*src)
{
*p = *src;
p++;
src++;
}
*p = '\0';
return dest;
}
// 自定义 strcat 函数,拼接字符串
char *my_strcat(char *dest, const char *src)
{
char *p = dest;
// 找到目标字符串的结束符
while (*p)
{
p++;
}
// 拼接源字符串
while (*src)
{
*p = *src;
p++;
src++;
}
*p = '\0';
return dest;
}
// 自定义 strcmp 函数,比较字符串
int my_strcmp(const char *str1, const char *str2)
{
while (*str1 == *str2 && *str1) {
str1++;
str2++;
}
return *str1 - *str2;
}
int main(int argc, const char *argv[])
{
char str[100] = "hello";
char str1[20] = " world";
// 测试 strlen 功能
size_t len = my_strlen(str);
printf("字符串 str 的长度为: %zu\n", len);
// 测试 strcpy 功能
char copy_str[100];
my_strcpy(copy_str, str);
printf("复制后的字符串为: %s\n", copy_str);
// 测试 strcat 功能
char cat_str[100];
my_strcpy(cat_str, str);
my_strcat(cat_str, str1);
printf("拼接后的字符串为: %s\n", cat_str);
// 测试 strcmp 功能
int ret = my_strcmp(str, str1);
printf("字符串比较结果为: %d\n", ret);
return 0;
}
- 指针指向字符串常量区的内容,不能通过指针修改
#include <stdio.h>
int main(int argc, const char *argv[])
{
// 定义一个字符指针p,并将其指向一个字符串常量"hello world"
char *p = "hello world";
// 注释解释:
// 字符串常量"hello world"被存储在只读数据段(.rodata段)中
// 该段的内容在程序运行期间不能被修改
// 尝试修改字符串常量的第一个字符为'a'
// *p = 'a'; // 这行代码会导致未定义行为,因为试图修改只读内存
// 如果取消注释这行代码,程序可能会崩溃或产生其他不可预测的行为
printf("1"); // 输出数字1
p = NULL; // 将指针p设置为NULL,表示它不指向任何有效的内存地址
// 尝试解引用NULL指针并打印其值
// 这将导致段错误(Segmentation Fault),因为NULL指针不指向任何有效的内存
printf("%c", *p); // 段错误
return 0;
}
【9】段错误
本质:非法访问内存
// 1. 修改常量区的内容
// 尝试修改字符串常量或其他只读数据段的内容会导致段错误
// 例如:char *p = "hello"; *p = 'a'; // 未定义行为,可能导致段错误
// 2. 野指针的间接访问(错误不能预知)
// 野指针是指未初始化或已被释放的指针
// 访问野指针指向的内存会导致不可预测的行为,通常会导致段错误
// 例如:char *p; printf("%c", *p); // p未初始化,访问*p可能导致段错误
// 3. 数组越界(错误不能预知)
// 访问数组中不存在的元素会导致数组越界
// 数组越界可能会导致程序崩溃或覆盖其他变量的内存
// 例如:int arr[5]; arr[5] = 10; // 越界访问,arr[5]不存在
// 4. 空指针的间接访问
// 访问空指针(NULL)指向的内存会导致段错误
// 例如:char *p = NULL; printf("%c", *p); // 访问*p会导致段错误
【10】思维导图
测试题:
- 输入带空格的字符串,删除字符串中的空格
#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[])
{
// 定义一个字符数组 str,大小为 100,用于存储输入的字符串
char str[100];
// 定义一个字符指针 p,后续用于指向字符串
char *p;
// 输入带空格的字符串
// scanf("%[^\n]", str); 是一种特殊的 scanf 用法
// %[^\n] 表示读取除换行符之外的所有字符,直到遇到换行符为止
// 这样就可以读取包含空格的字符串,并将其存储到 str 数组中
scanf("%[^\n]", str);
// 指针 p 指向字符串的起始位置
// 让指针 p 指向字符数组 str 的首地址,方便后续通过指针操作字符串
p = str;
// 定义两个整型变量 i 和 j,用于遍历字符串和构建新字符串
int i, j;
// 遍历字符串
// 使用 for 循环遍历字符数组 str,i 用于遍历原字符串,j 用于构建新字符串
// 当 str[i] 不等于字符串结束符 '\0' 时,继续循环
for (i = 0, j = 0; str[i] != '\0'; i++) {
// 判断当前字符是否为空格
if (str[i] != ' ') {
// 如果当前字符不是空格,则将其复制到新字符串的位置
// 将 str[i] 的字符赋值给 str[j],即把非空格字符依次放到新位置
str[j] = str[i];
// j 自增,指向下一个新字符串的存储位置
j++;
}
}
// 新字符串结束标志
// 当遍历完原字符串后,在新字符串的末尾添加字符串结束符 '\0'
// 表示新字符串的结束
str[j] = '\0';
// 输出去除空格后的字符串
printf("%s", str);
// 程序正常结束,返回 0
return 0;
}