一、指针基础
1.概念
- 计算机中的内存中的每一个字节都有一个地址,地址用16进制表示。一般可以理解为指针就是地址,地址就是指针。
- 定义指针
int* ptr;//定义了一个指向整数的指针,ptr中可以存储int类型的首个字节的地址,int就是让它连续读sizeof(int)个字节。
--------------------------------------------
int a =10;
int* ptr = &a;//ptr现在存储的是a的地址,是int*类型
----------------------------------------------
int value = *ptr //ptr解引用,也就是降级,故value被赋值为10
“*”是间接访问操作符,作用是通过指针访问存储在该地址上的值
- 指针是一个变量,它存储了另一个变量的内存地址。换句话说,指针本身存储的是一个内存地址,而不是一个具体的值。
- 指针有类型,这意味着它指向的是特定类型的数据。例如,int* 是一个指向整数的指针,char* 是一个指向字符的指针,double* 是一个指向双精度浮点数的指针。
2.野指针
- 野指针(dangling pointer)是指向已释放或未分配内存区域的指针。它们通常是在以下情况下产生的:
1.指针没有初始化:指针变量被声明但未初始化,指向一个不确定的位置。
#include <stdio.h>
int main() {
int* ptr; // 未初始化的指针,指向不确定的位置
*ptr = 10; // 未定义行为,可能会导致程序崩溃
return 0;
}
2.指针指向的内存被释放:指针指向的内存被释放后,指针未被置为 NULL 或未重新指向有效内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存
*ptr = 10;
free(ptr); // 释放内存
*ptr = 20; // 未定义行为,ptr 成为野指针
return 0;
}
3.超出作用域:局部指针变量指向的内存超出其作用域,导致指针悬挂
#include <stdio.h>
int* danglingPointer() {
int localVar = 10;
return &localVar; // 返回局部变量的地址,局部变量在函数返回后被销毁
}
int main() {
int* ptr = danglingPointer();
printf("%d\n", *ptr); // 未定义行为,ptr 是一个野指针
return 0;
}
- 避免野指针的方法
1.初始化指针:声明指针时将其初始化为 NULL。
2.释放内存后将指针置为 NULL:释放动态内存后,将指针置为 NULL。
3.慎用局部指针返回值:不要返回指向局部变量的指针。可以使用动态内存分配或者静态变量解决此问题。
4.定期检查指针有效性:在使用指针前,检查其是否为 NULL。
if (ptr != NULL) {
// 安全使用指针
}
指针变量
1.存储指针的变量称之为指针变量,即指针变量空间中存储的是地址。
2.指针变量中存储的都是其他变量的首地址。
3.存储一级指针地址的指针变量,称之为二级指针。依次类推,就是多级指针
int*** ptr3 ---->ptr3中可以存储int**类型变量空间的首地址,ptr3自己则是int ***类型
4.指针变量的大小与指针变量类型没有关系,而是与操作系统有关系,32位操作系统地址空间最大为2^32,即指针变量为4个字节。64位操作系统依此类推为8个字节。
5.&取变量首地址,但无法获取常量的地址,打印地址则用%p
6.指针变量定义时可以给一个具体的指针,如果现在没有具体的值,那么将该指针指向null,占位。
int* pe;
pe = &e;//等价于int* pe=&e;
---------------------------------------------------------
int* pc=&b;//将b变量的首地址,赋值给pc,此时可以说pc指向b
int* pd=pc;//将pc空间的数据赋值给pd,那么可以说pd和pc指向同一个内存空间
7.在口头表述上,我们将取地址称之为升级,解引用称之为降级
取一次地址,类型会多一个*,例如int a升级后会成为int*类型。 int* ptr升级后会变成int**类型
解引用一次,类型会少一个*,例如int** ptr降级后会成为 int*类型。int* ptr降级后会变成int类型。
指针数组
存储指针的数组,称之为指针数组
int* brr[4]; ==>数组的容量为4为,其中的每一个元素都是int*类型
数组指针
1.数组指针 :这是一个指针,指向整个数组。
3.把整个数组当成一个整体(当成一个数据),数组指针就存储的这个数据的首地址。
2.定义
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // p 是一个指向包含 5 个 int 的数组的指针
内存地址:arr 和 &arr 指向相同的内存地址,即数组的起始位置。
类型:arr 的类型是 int (*)[4],而 &arr 的类型是 int (*)[3][4]。
用途:它们在某些上下文中可以互换使用,但要注意类型的差异。例如,作为函数参数时,通常会使用 int (*)[4] 而不是 int (*)[3][4]。
4.取整个数组的首地址:&数组名,但是用printf打印(%p打印地址值)是一样的,但是他们的数据类型不一样
arr是int*类型
p则是 int (*p)[5]类型
5.arr+1是偏移一个元素,p+1则是偏移一个数组。
6.*p 的类型是 int*,即指向数组首元素的指针。在这里,*p 等价于 arr 或 &arr[0]
6.*p代表将p数组指针降级(解引用)为指向数组首元素的指针。
d=(*p)[0]; //将数组arr[0]的值赋值给d,即*p等价于arr,故为一级指针,也可以写成d=*((*p)+0)
7.范例
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // 定义一个指向数组 arr 的指针
// 使用 *p 访问数组元素
for (int i = 0; i < 5; i++) {
printf("(*p)[%d] = %d\n", i, (*p)[i]);
}
// 将 *p 赋值给 int* 指针
int* ptr = *p;
for (int i = 0; i < 5; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}
return 0;
}
8.int* ptr = *p; 将 *p 赋值给一个指向整数的指针 ptr,使得 ptr 可以像普通指针那样遍历数组元素。
指针与二维数组
1.可以理解为数组的数组
2.二维数组在内存中是以连续的块存储的,也就是说,所有的元素按行优先(row-major order)顺序存储。即二维数组是连续的
3.二维数组用指针访问
#include<stdio.h>
int main(){
int arr[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
int (*p)[4] = arr; // p是一个指向包含4个 int 的数组的指针
for(int i=0; i<3; i++)
{
for(int j=0; j<4;j++) // 输出四种不同方式访问二维数组元素的结果
printf("%2d %2d %2d %2d\t",*(*(p+i)+j),(*(p+i))[j],*(p[i]+j),p[i][j]);
putchar('\n');
}
}
----------------------------------------------
*(*(p+i)+j)---------->先对p做偏移,即移动以一维数组的大小为单位,然后再解引用,此时*(p+i)为第i行首元素地址,再在第i行内偏移j个字节,再解引用一次,此时效果相当于p[i][j]
(*(p+i))[j]---------->[]也可以降级,即解引用,但是一定要注意的是*(p+i)一定要用圆括号括起来,避免步骤错误。[]一定要是一维数组首元素地址
*(p[i]+j)------------>p[i]实际上等同于*(p+i),即第i行首元素地址。
在表达式中,arr 可以隐式转换为 int (*)[4],即指向包含 4 个 int 元素的一维数组的指针。
p 不是一个二维指针,而是一个指向一维数组的指针。具体来说,p 是一个指向包含 4 个 int 元素的数组的指针,其类型是 int (*)[4]。
- 列指针与行指针
行指针本质上就是一个数组指针,能够指向一整行。
对数组指针进行降级后,会指向数组的首元素。
二维数组的名字就是二维数组的首行地址
所以对行指针进行降级后,会指向数组内的首元素。将行指针降级后的指针称之为列指针。
4.区别
int *q[5];//此时是指针数组,等价于int *(q[5])
int (*q)[5];//此时为数组指针
arr 和 &arr[0][0] 在内存地址上可能相同,但它们的类型和用途不同。
arr 是指向二维数组行的指针,而 &arr[0][0] 是指向第一个元素的指针。
&arr[0][0] 作为指向第一个元素的指针,可以按一维数组的方式线性访问整个数组的所有元素。
指针与函数
指针的传值和传指
传值
- 将变量空间中存储的数据传入到函数,对传递前后对实参不会产生影响
- 传入的值会赋给临时的局部变量,函数内部是对这个临时的局部变量进行操作
传指
- 将变量的首地址传入函数,函数内部可以通过该地址,访问到外部的存储空间,因此就可以改变函数外部实参中的数据
数组的传递
- 数组传递的时候,无法将整个数组传入函数。而是将数组的首元素地址传入函数。是个传指的过程
void func1(int* parr) //int* parr:需要传入一个int*类型数据
void func1(int arr[]) //int arr[]实际上是一个int类型指针:int*
void func3(int arr[200]) //int arr[200]:在形参位置代表int*类型指针。
-
%s在printf中打印时,会遇到’\0’停止
-
int arr[]在形参位置上本质是一个int* 指针,另外可以提醒用户传入数组首地址。
main函数的参数
#include<stdio.h>
int main(int argc,const char* argv[])
-------------------------------------------
argc的值是位置变量的个数,包含了./a.out
argv指向的是一个指针数组,数组中每个元素的都是char*类型,char*类型又指向着以字符串形式存储的,位置变量的内容。
- "345"作为右值赋值给非字符串数组时是相当于给了一个地址
char *ptr = "hello";
//会将字符串常量 "hello" 的地址赋给字符指针 ptr
指针函数
- 返回值是指针类型的函数,本质上是一个函数
函数指针
- 指向函数的指针。本质上是一个指针,它允许你动态地调用不同的函数。
int (*p)(int,int);//p可以指向一个函数,该函数返回值是必须是int类型,参数列表必须是(int,int)类型
- 函数的名字就是函数的首地址,这和数组很像。
#include <stdio.h>
// 定义一个函数,返回值为整数,接受两个整数参数
int add(int x, int y) {
return x + y;
}
// 定义一个函数,返回值为字符指针,接受两个字符指针参数
char *concat(char *str1, char *str2) {
// 在堆上分配内存来存储合并后的字符串
char *result = malloc(strlen(str1) + strlen(str2) + 1); // +1 是为了存储字符串结束符 '\0'
// 检查内存分配是否成功
if (result == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
// 将两个字符串拼接到一起
strcpy(result, str1);
strcat(result, str2);
return result;
}
int main() {
int a = 5, b = 3;
int *p_int = &a; // p_int 指针指向整型变量 a 的地址
int (*p_array)[4]; // p_array 是一个指向包含4个整数的数组的指针
int (*p_func)(int, int); // p_func 是一个指向接受两个整数参数并返回整数的函数的指针
char *(*p_strfunc)(char *, char *); // p_strfunc 是一个指向接受两个字符指针参数并返回字符指针的函数的指针
// 使用指针 p_int 来访问变量 a 的值
printf("Value of variable a: %d\n", *p_int); // 输出: 5
// 将 p_array 指向一个包含 4 个整数的数组
int arr[4] = {1, 2, 3, 4};
p_array = &arr;
// 使用指针 p_array 访问数组的元素
printf("Value of first element in array: %d\n", (*p_array)[0]); // 输出: 1
// 将 p_func 指向函数 add
p_func = add;
// 调用函数指针 p_func 所指向的函数
int sum = (*p_func)(a, b); // 等价于 sum = add(a, b)
printf("Sum of a and b: %d\n", sum); // 输出: 8
// 将 p_strfunc 指向函数 concat
p_strfunc = concat;
// 调用函数指针 p_strfunc 所指向的函数
char *result = (*p_strfunc)("Hello, ", "world!"); // 等价于 result = concat("Hello, ", "world!")
printf("Concatenated string: %s\n", result); // 输出: Concatenated string: Hello, world!
// 释放动态分配的内存
free(result);
return 0;
}
回调函数
- 将函数A的地址作为参数传入另外一个函数B中,该函数A被称之为回调函数。此时B中有A的地址,所以在B中可以调用函数A。
指针常量和常量指针
- const修饰的数据类型会变为常类型,用常类型定义的变量,其中存储的数据不允许被修改,只读的。让变量常量化。
- const修饰普通变量
const int b=10;
int const b=10;//两者等价,b均不允许被修改
指针常量
- 指针是一个常量,这表明指针空间中存储的地址值不允许被修改。但是指针指向的那块空间中的内容可以修改
int* const ptr;
常量指针
从Linux虚拟机粘贴到Windows中
1.先同时按下ctrl + shift,然后拖动鼠标选择要复制的内容,选择之后再按下c(注意这个过程ctrl +shift在按下c的过程中不要松)
2.在Windows中直接按下ctrl+v即可
也可以通过共享文件夹的方式
Linux中共享文件夹的位置是
/mnt/hgfs/共享文件夹
cp 复制的文件(在该目录下) /mnt/hgfs/共享文件夹/可以重命名
在Linux中找文件夹,点其他位置,再点击计算机,此时就可以查看所有文件了
tips
void GetMemory(char *p)//函数错误,传入p(p实际上是地址,类型为char *型)
{
p=(char*)malloc(100);
}
void Test(void)
{
char *str=NULL;
GetMemory(str);//此时相当于p=str;p=char(char *)malloc(100);因此str没有作用
strcpy(str,"hello world");//向NULL指针空间拷贝"hello world"必出段错误
printf(str);//格式不对,而且用malloc申请堆空间,必用free手动释放
- 有int a=10;int* pa=&a;
则(&a)++和pa++的区别
(&a)++ : 非法的。&a是常量,常量无法作为左值。
pa++: 合法的。pa是变量,pa++相当于是对pa中存储的地址值做++运算。int b=10; b++; - 指针运算
- 对指针进行算数运算,实际上是对指针进行偏移
- 对指针进行+1操作,偏移量是:指针指向的数据类型的大小
int* pa . pa+1,偏移4个字节
char* pb; pb+1,偏移1个字节
short* pc; pc+1,偏移2个字节
long* pd; pd+1, 偏移8个字节(64位OS) ,偏移4个字节(32位OS) - *号和++运算符优先级为同级,但是是自右向左计算
pa++ : 先计算pa++,后置++先返回pa的值,然后将pa自增。*号是和偏移前的pa结合
*(pa++):同pa++
*++pa : 先计算++pa,是个前置++(先pa自增,pa先偏移到0xa5,再返回pa的值)
- 指针与字符串
1. 指针与字符串,就是指针与一维数组。因为字符串是存储在char类型数组中。
2. "1234"是常量字符串,常量字符串单独使用代表了常量字符串的首地址。
3. %s占位符,对应的数据类型char*类型。原理:从给定的字符地址位置开始打印,直到遇到’\0’字符停止
4. 未定义行为
char *p = "hello";//这一行代码定义了一个指向字符串常量 "hello" 的指针 p。在C语言中,字符串常量通常存储在只读内存区域中,因此通过指针 p 修改字符串常量的内容是未定义行为,会导致程序崩溃或者异常。
*p = 'a';//非法行为
p++;//可以正常运行
----------------------------------
如果希望修改字符串的内容,应将其定义为一个字符数组,而不是指向字符串常量的指针。字符数组存储在可写的内存区域中,允许修改其内容。
- 动态分配内存
- 引入头文件
#include <stdlib.h>
- 调用 malloc 函数
void* ptr = malloc(size);
- 检查 malloc 的返回值
if (ptr == NULL) {
// 处理内存分配失败的情况
}
-
使用分配的内存
-
释放分配的内存
free(ptr);
6.范例:分配一个结构体数组
#include <stdio.h> // 包含标准输入输出库头文件,用于使用 printf 函数
#include <stdlib.h> // 包含标准库头文件,用于使用 malloc 和 free 函数
// 定义一个名为 Point 的结构体,包含两个整数成员 x 和 y
typedef struct {
int x;
int y;
} Point;
int main() {
int n = 3; // 定义一个整数 n 并初始化为 3,表示我们将分配 3 个 Point 结构体的内存
// 使用 malloc 函数动态分配 n 个 Point 结构体大小的内存
// malloc 返回一个 void* 类型的指针,这里需要将其转换为 Point* 类型
Point *points = (Point*)malloc(n * sizeof(Point));
// 检查 malloc 返回的指针是否为 NULL,以确保内存分配成功
if (points == NULL) {
printf("Memory allocation failed\n"); // 如果内存分配失败,打印错误信息
return 1; // 返回非零值表示程序异常终止
}
// 使用 for 循环初始化分配的 Point 结构体数组
for (int i = 0; i < n; i++) {
points[i].x = i; // 设置第 i 个 Point 的 x 成员为 i
points[i].y = i * 2; // 设置第 i 个 Point 的 y 成员为 i 的两倍
}
// 使用 for 循环打印每个 Point 结构体的成员值
for (int i = 0; i < n; i++) {
printf("Point %d: (%d, %d)\n", i, points[i].x, points[i].y); // 打印第 i 个 Point 的 x 和 y 值
}
// 使用 free 函数释放先前分配的内存,防止内存泄漏
free(points);
return 0; // 返回 0 表示程序成功执行
}
- 内存分布
- 代码段(Text Segment)
存储程序的可执行代码(机器指令)。
通常是只读的,以防止程序意外修改其指令。 - 数据段(Data Segment)
分为已初始化数据段和未初始化数据段。
已初始化数据段(Initialized Data Segment):存储已初始化的全局变量和静态变量。
未初始化数据段(BSS Segment):存储未初始化的全局变量和静态变量,程序启动时自动初始化为零。 - 堆(Heap)
用于动态内存分配,程序运行时可以使用 malloc、calloc、realloc 等函数从堆中分配内存,使用 free 释放内存。
堆内存的分配和释放由程序员控制,不受作用域限制。 - 栈(Stack)
用于存储函数调用的上下文(局部变量、函数参数、返回地址等)。
栈内存自动管理,函数调用时分配内存,函数返回时释放内存。
栈内存有固定的大小限制,过深的递归调用或过大的局部变量分配可能导致栈溢出。 - 常量区(Literal or Constant Segment)
存储程序中的常量,包括字符串字面量和 const 变量。
这些常量通常是只读的,防止程序修改常量的值。