指针
指针基本介绍
变量在内存中的存储
如图中右侧图形表示计算机内存(memory),图形中每一个长条表示一个字节(byte),每一个字节存在对应的一个地址,如左侧0、201、202...209所标注
对于典型的现代计算机,1个int
类型变量由4个字节表示,1个char
类型变量由1个字节表示,1个float类型变量由4个字节表示。对于如下代码:
int a;
char c;
a = 5;
a++;
在内存中,如内存模型所示:计算机分配地址为204-207共4个字节的存储变量a
,分配地址为209的1个字节存储变量c
,当给a
赋值5时,将在地址204-207的内存处存储5(在计算机中数字以二进制保存),a
自加1则会对地址204-207内存位置存储的5加1。
指针变量在内存中的储存
指针:指针是一个变量,存放着另外一个变量的地址
对于如下代码:
int a;
int *p;
p = &a;
a = 5;
printf("%d\n", p); // 204
printf("%d\n", &a); // 204
printf("%d\n", &p); // 64
printf("%d\n", *p); // 5
*p = 8;
printf("%d\n", a); // 8
结合下图右侧内存模型。首先计算机分配内存地址204-207存储a
,第二行定义变量p
,变量p
是一个 int*
类型的变量,其内存地址为64-67(对于32位系统,使用4个字节表示一个内存地址),该变量存储了一个int
类型变量的地址,随后&a
表示获取了变量a
的地址并赋值给变量p
,即指针变量p
中存储的值为变量a
的首地址204。
- 当我们打印
p
的值时输出为204 - 当打印
a
的地址&a
时输出为204 - 当打印p的地址
&p
时输出为64 - 当打印变量
p
的解引用*p
时(解引用表示,取出变量p
内存储的地址204所存储的变量a
)输出为5 - 当对
*p
赋值为8后,即修改了变量a
的值,此时打印a
输出为8。
指针代码示例
示例一:注意初始化
#include <stdio.h>
int main(void)
{
int a; // 未初始化自动变量,存储为垃圾值
int *p; // 未初始化话指针,直接使用可能会导致程序崩溃
a = 10;
p = &a;
printf("%d\n", p); // 3077992
printf("%d\n", *p); // 10
printf("%d\n", &a); // 3077992
return 0;
}
示例二:通过指针变量修改指向对象的值
#include <stdio.h>
int main(void)
{
int a = 10;
int *p;
p = &a;
printf("%d\n", a); // 10
int b = 12;
*p = b;
printf("%d\n", a); // 12
return 0;
}
示例三:指针越界
#include <stdio.h>
int main(void)
{
int a = 10;
int *p;
p = &a;
printf("%d\n", p); // p is 2002(变量a的首地址)
printf("%d\n", *p); // 10
printf("size of integer is %d bytes\n", sizeof(int)); // 4个字节
printf("%d\n", p + 1); // p+1 is 2006(变量a后面下一个int类型变量的首地址)
printf("%d\n", *(p + 1)); // -858993460, 指针越界
return 0;
}
指针的类型
如上图所示,对于一个整型变量a
(见图片右侧中间部分),当赋值1025时,其对应的四个字节200-203存储的二进制数字分别为00000000 00000000 00000100 00000001,如图中标注(小端,字幕标注为LSB)。
最高位为符号位:
- 0代表正数,1代表负数
指针是强类型:
- 假设p为int*类型,打印p,即200(变量a的首地址)。打印*p,机器认为int为4个字节,按序读取4个字节是数据,结果为1025。
- 假设p为char*类型,打印p,即200。打印*p,机器认为char为1个字节,按序读取1个字节是数据,结果为1。
int a = 1025;
int *p;
p = &a;
printf("sizeof of integer is %d bytes\n", sizeof(int)); // 4
printf("Address = %d, value = %d\n", p, *p); // 200, 1025
printf("Address = %d, value = %d\n", p + 1, *(p + 1)); // 204, -2454153
char *p0;
p0 = (char*)p; // 强制类型转换
printf("sizeof of char is %d bytes\n", sizeof(char)); // 1
printf("Address = %d, value = %d\n", p0, *p0); // 200, 1
printf("Address = %d, value = %d\n", p0 + 1, *(p0 + 1)); // 204 4
// 1025 = 00000000 00000000 00000100 00000001
-
(char*)
是一种强制类型转换,表示把int*
类型变量p
强制转换为char*
。 -
当打印
p
时输出为变量a
的地址200,当打印*p
时输出为变量a
的值1025,当打印p+1
时输出为变量a
下一个int类型变量地址204,当打印*(p+1)
时输出为未初始化的随机int
类型值比如为-2454153。当打印p0
时输出变量a
的首个字节的地址200,当打印*p0
时输出地址200存储的数值0b00000001(0b表示二进制)即为1,当打印p0+1
时输出地址200后一个char
类型变量的地址201,当打印*(p0+1)
时输出地址201存储的数值0b00000100即为4。 -
关键:不同类型的指针在解引用时,所读取的字节数于指针所指向的数据类型相关。比如:如果为
int*
则读取该地址及后面共4个字节存储的数据,指针变量+1则地址数值+4;如果为char*
则读取该地址1个字节存储的数据,指针变量+1则地址数值+1,可以通过sizeof
关键字确定类型大小。
void指针
#include <stdio.h>
int main(void)
{
int a = 1025;
int *p;
p = &a;
printf("sizeof of integer is %d bytes\n", sizeof(int));
printf("Address = %d, value = %d\n", p, *p);
void *p0;
p0 = p; // 合法
//printf("Address = %d, value = %d\n", p0, *p0); // 编译出错,不能打印void*类型指针的值
// printf("Address = %d, value = %d\n", p0 + 1, *(p0 + 1)); // 出错,void*类型的指针不能进行算术运算
printf("Address = %d\n", p0); // 合法
return 0;
}
如上代码所示,无需类型转换,可以将任意类型变量的地址赋值给void*
类型指针,但是该类型指针只能打印地址,无法使用解引用*
以及+1
等运算操作。
指向指针的指针
函数传值
#include <stdio.h>
void Increment(int a)
{
a = a + 1;
printf("Address of variable a in increment = %d\n", &a);
}
int main(void)
{
int a;
a = 10;
Increment(a);
printf("a = %d\n", a); // 10
printf("Address of variable a in increment = %d\n", &a);
return 0;
}
实参:在主调函数中调用其他函数,这个参数称为实参
形参:被调函中的这个参数,被称为形参
传值调用:本质是把一个变量映射到另外一个变量,一个变量中的值拷贝到另一个函数
传引用
#include <stdio.h>
void Increment(int *p)
{
*p = (*p) + 1;
}
int main(void)
{
int a;
a = 10;
Increment(a);
printf("a = %d\n", a); // 11
return 0;
}
传引用的好处:可以节省更多的内存空间,避免复杂数据类型的拷贝
应用程序内存
- Code(Text):存储程序指令,计算机需要将指令加载到内存,例如程序中的自增语句
- Static/Global:存储静态或全局变量
- Stack:存储局部变量
- Heap:动态内存
其中前3个段,即代码段、静态/全局变量段和栈段是固定的,在应用程序开始时确定,但是在应用程序运行时,可以要求在堆中分配更多的内存
每个函数都有一个单独的栈帧
- 执行到Increment函数,暂停主程序,执行Increment函数,为其单独开辟一块栈帧,传进来的a会被拷贝到新的内存中,不能访问自己栈帧之外的变量,执行完成后,清除Increment函数,并且返回主程序继续执行
- 栈空间在程序开始时确定,假设一个函数无限次调用另一个函数,即无限递归,那么栈将会溢出,程序会崩溃
指针和数组
数组名:指向数组首元素的指针常量(不可修改指向的地址),即数组的基地址
地址:&A[i] == (A +i),二者等价
值:A[i] == *(A + i),二者等价
#include <stdio.h>
int main()
{
int A[] = {2, 4, 5, 8, 1};
int i;
// A++; // 非法,数组名为指针常量
int *p = A; // 合法
for(i = 0; i < 5; i++)
{
printf("Address = %d\n", &A[i]);
printf("Address = %d\n", A + i);
printf("value = %d\n", A[i]);
printf("value = %d\n", *(A + i));
}
}
数组作为函数参数
#include <stdio.h>
int SumOFElements(int A[], int size)
{
int i, sum = 0;
for(i = 0; i < size; i++)
{
sum += A[i];
}
return sum;
}
int main(void)
{
int A[] = {1, 2, 3, 4, 5};
int size = sizeof(A) / sizeof(A[0]); // 获取数组元素个数
int total = SumOFElements(A, size);
printf("Sum of elements = %d\n", total);
}
下面程序在函数内部计算数组大小,结果出错
#include <stdio.h>
int SumOFElements(int A[])
{
int i, sum = 0;
int size = sizeof(A) / sizeof(A[0]);
printf("SOE - Size of A = %d, size of A[0] = %d", sizeof(A), sizeof(a[0]);
for(i = 0; i < size; i++)
{
sum += A[i];
}
return sum;
}
int main(void)
{
int A[] = {1, 2, 3, 4, 5};
int total = SumOFElements(A); // 此处传A等价于传&A[0]
printf("Sum of elements = %d\n", total);
printf("SOE - Size of A = %d, size of A[0] = %d", sizeof(A), sizeof(a[0]);
}
理想状态下,函数栈帧中的A为一个数组,其中元素和主调函数中的元素相同,占据4个int类型的空间
事实上,编译器看到数组作为参数,不会拷贝整个数组,实际仅仅创建一个同名指针(而不是创建整个数组),指向主调函数的数组首元素的地址,编译器隐式的将int A[]转换为了int *A,即被调函数中的A不是被解释成一个数组,而是解释成一个整型指针
数组名作为参数,仅仅拷贝变量的地址,而不是变量的值,即传引用,防止每次拷贝整个数组浪费大量的内存
指针和字符数组
字符数组以空字符'\0'结束,所以需要预留结尾的空字符的位置
#include <stdio.h>
#include <string.h>
int main(void)
{
// 方法一
// char C[5] = {'J', 'O', 'H', 'N', '\0'};
/* 方法二
char C[5];
C[0] = 'J';
C[1] = 'O';
C[2] = 'H';
C[3] = 'N';
C[4] = '\0';
*/
// 方法三
char C[] = "JOHN"; // 自动计算大小
/* 非法,必须写在一行
char C[20];
C = "JOHN"; // 出现const char[] 到 char[]的类型转换,不允许const转为非const
*/
int len = strlen(C); // 不会计算末尾的空字符
printf("Length = %d\n", len);
}
指针和字符数组的不同之处:
-
字符数组名为指针常量,不允许指向别处
C1 = C2; // 不允许
C1++; // 不允许
指针常量和常量指针
const int *c; // 常量指针,只读
int *const c; // 指针常量,不可修改指针指向的地址
指针和二维数组
int B[2][3];
// B返回一个指向一维数组(其中包含3个整型元素)的指针
int *p = B; // 错误,指针类型不匹配
-
打印 B 或 &B[0] // 400, 类型为int (*)[3]
-
打印 *B 或 B[0] 或 &B[0][0] // 400, 类型为int
-
*打印 (B + 1) + 2 或 B[1] + 2 或 &B[1][2] // 412 + 8
*(B + 1)的类型为int*,+2就是移动到下下个整型类型,跳过8个字节
-
打印 * (*B + 1) 或 B[0][1] // 3
指针和多维数组
- 多维数组本质上就是数组的数组
- 多维数组可以理解为数组的合集
int C[3][2][2];
int (*p)[2][2] = C;
打印 C // 800, 类型为 int (*)[2][2]
打印 *C 或 C[0] 或 &C[0][0] // 800,类型为 int (*)[2]
*(C[0][1] + 1) == C[0][1][1]; // 9, C[0][1]类型为int*
*(C[1] + 1) == C[1][1] == &C[1][1][0]; // 824,C[1]类型为int (*)[2]
#include <stdio.h>
int main()
{
int C[3][2][2] = {{{2, 5}, {7, 9}},
{{3, 4}, {6, 1}},
{{0, 8}, {11, 13}}};
printf("%d %d %d %d", C, *C, C[0], &C[0][0]);
printf("%d", *(C[0][0] + 1));
}
多维数组传参
#include <stdio.h>
// 错误的形参
// void Func(int **A) 对二维数组,传一个指针的指针
// void Func(int (*A)[2][2]) 对三位数组,传一个指针的指针的指针
void Func(int (*A)[2][2]) // 数组的第一维可以省略,其他维必须指定
{
}
int main()
{
int C[3][2][2] = {{{2, 5}, {7, 9}},
{{3, 4}, {6, 1}},
{{0, 8}, {11, 13}}};
int A[2] = {1, 2};
int B[2][3] = {{2, 4, 6}, {5, 7, 8}};
/*
int X[3][2][3]; // 维度不匹配,报错
Func(X);
*/
Func(C);
}
指针和动态内存 - 栈 vs 堆
栈
执行square函数
square函数执行完毕,占用的栈上的内存被清除(销毁),进而执行SquareOfSum函数
重复上述步骤,直到栈空
内存在栈上分配和销毁的规则:
- 当一个函数被调用,它被压入栈中
- 结束时,弹出堆栈
堆
- 动态内存,对比栈来说,内存大小不固定
- 没有特定的规则来分配和销毁相应内存
- 自由分配内存大小,注意不要超出系统内存范围
- 此处的堆特指“空闲的内存池”(与数据结构中的堆进行区分)
- 需要手动释放内存空间,否则会导致内存泄漏
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a; // goes on stack
int *p;
p = (int*)malloc(sizeof(int));
*p = 10;
free(p); // 释放malloc分配的内存,防止内存泄漏
p = (int*)malloc(sizeof(int));
*p = 20;
}
如果malloc找不到空闲的内存块,即不能在堆上成功分配内存,返回NULL
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a; // goes on stack
int *p;
p = new int; // new和delete操作符是类型安全的
*p = 10;
delete p;
p = new int[20];
delete[] p;
}
malloc, calloc, realloc, free
malloc
作用:
- 在堆中分配内存
函数原型:
void* malloc(size_t size) // size_t可以理解为unsigned int类型
函数用法:
int *p = (int*)malloc(3 * sizeof(int));
calloc
作用:
- 在堆中分配内存
函数原型:
void* malloc(size_t num, size_t size) // 第一个参数用于指定类型的元素的数量,第二个参数用于指定类型的大小
函数用法:
int *p = (int*)malloc(3, sizeof(int));
malloc和calloc的区别:
- 使用calloc函数,元素会自动初始化为0
- 使用malloc函数,元素不会初始化,而是随机值
realloc
作用:
- 在堆中分配内存
函数原型:
void* malloc(void* Ptr, size_t size) // 第一个参数用于指向已分配内存的起始地址,第二个参数用于指定新内存块的大小
函数用法:
int *p = (int*)malloc(3, sizeof(int));
例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n;
printf("Enter size of array\n");
scanf("%d", &n);
int *A = (int*)malloc(n * sizeof(int));
for(int i = 0; i < n; i++)
{
A[i] = i + 1;
}
int *B = (int*)realloc(A, 2*n*sizeof(int));
printf("Prev block address = %d, new address = %d\n", A, B);
for(int i = 0; i < 2 * n; i++)
{
printf("%d\n", B[i]);
}
}
动态分配 - 内存泄漏
// "Simple Betting game'
// "Jack Queen King" - computer shuffles these cards
// player has to guess the position of queen.
// if he wins, he takes 3*bet
// if he looses, he looses the bet amount .
// player has $100 initially
#include<stdio.h>
#include<stdlib.h>
int Cash = 100;
void Play(int bet)
{
/* 方式一:
char C[3] = {'J', 'Q', 'K'};
*/
/* 方式二:
char* C = (char*)malloc(3 * sizeof(char));
C[0] = 'J'; C[1] = 'Q'; C[2] = 'K';
*/
printf("Shuffling ...");
srand(time(NULL));
int i;
for(i = 0; i < 5; i++)
{
int x = rand() % 3;
int y = rand() % 3;
int temp = C[x];
C[x] = C[y];
C[y] = temp;
}
int PlayerGuess;
printf("What's the position queen 1, 2, 3");
scanf("%d", &PlayerGuess);
if(C[PlayerGuess - 1] == 'Q')
{
Cash += 3 * bet;
printf("You Win ! Result = %c%c%c Total Cash = %d", C[0], C[1], C[2]);
}
else
{
Cash -= bet;
printf("You Loose ! Result = %c%c%c Total Cash = %d", C[0], C[1], C[2]);
}
free(C); // 回收堆上的内存
}
int main()
{
int bet;
while(cash > 0)
{
printf("What's your bet? $");
scanf("%d", &bet);
if(bet == 0 || bet > cash) break;
Play(bet);
printf("\n****************\n");
}
}
方式一:函数中的局部变量在栈中分配内存,每次调用会自动回收内存,不会导致内存泄漏
方式二:函数中的局部变量在堆中分配内存,如果不手动释放,每次调用函数都会导致程序占用内存空间增大,即内存泄漏
总结:内存泄漏是因为堆中未使用和未引用的内存块,栈上的内存是自动回收的,栈的大小是固定的,最多发生栈溢出
函数返回指针
函数返回指针常用于:指向堆中分配的内存空间
// Pointers as function returns
#include <stdio.h>
#include <stdlib.h>
void PrintHelloWorld()
{
printf("Hello World\n");
}
int* Add(int* a, int* b) // 被调函数
{
int c = (*a) + (*b);
return &c;
}
int main() // 主调函数
{
int a = 2, b = 4;
int* ptr = Add(&a, &b);
PrintHelloWorld();
printf("Sum = %d\n", *ptr);
}
修改Add函数
int* Add(int* a, int* b) // 被调函数
{
int* c = (int*)malloc(sizeof(int));
*c = (*a) + (*b);
return &c;
}
函数指针
程序执行过程:将源代码作为编译器的输入,再由编译器编译出机器代码
内存:一般指程序运行的上下文
主存:随机存储器RAM
在内存中,一个函数就是一块连续的内存(里面是指令),函数的地址,也称为函数的入口点,是函数的第一条指令的地址
#include <stdio.h>
int Add(int a, int b)
{
return a + b;
}
int main()
{
int c;
int (*p)(int, int); // ()优先级高于*
/* 方式一
p = &Add;
c = (*p)(2, 3);
*/
// 方式二
p = Add;
c = p(2, 3);
printf("%d", c); // 5
}
错误用法一:
void (*p)(int, int); // 类型不匹配
错误用法二:
int (*p)(int); // 参数个数不匹配
函数指针的使用案例(回调函数)
示例一:
//Function Pointers and callbacks
#include <stdio.h>
void A()
{
printf("He1lo");
}
void B(void (*ptr)()) // function pointer as argument
{
ptr(); //call-back function that "ptr" points to
}
int main()
{
void (*p)() = A;
B(p);
}
示例二:
#include <stdio.h>
#include <math.h> // 包含abs函数
int compare(int a, int b)
{
if(a > b) return 1;
else return -1;
}
int absolute_compare(int a, int b)
{
if(abs(a) > abs(b)) return 1;
else return -1;
}
void BubbleSort(int *A, int n, int (*compare)(int, int))
{
int i, j, temp;
for(i = 0; i < n; i++)
for(j = 0; j < n - 1; j++)
if(compare(A[j], A[j + 1] > 0)
swap(A[j], A[j + 1]);
}
int main()
{
int i, A[] = {3, 2, 1, 5, 6, 4};
BubbleSort(A, 6, compare);
for(i = 0; i < 6; i++) printf("%d ", A[i]);
}
示例三:
#include <stdio.h>
#include <stdlib.h> // 包含qsort函数
int compare(const void* a, const void* b)
{
// a是一个整型列表,首先需要将通用指针转换为整型指针,再通过*解引用得到a的值
int A = *((int*)a);
int B = *((int*)b);
return A - B; // 升序排列
// return B - A; 降序排列
}
int main()
{
int i, A[] = {3, 2, 1, 5, 6, 4};
qsort(A, 6, sizeof(int), compare);
for(i = 0; i < 6; i++) printf("%d ", A[i]);
}
指针及其应用
内存是可寻址的字节
- 内存是一个字节数组
- 每个字节都有一个唯一的地址(字节可寻址)
- 寻址的最小数据对象是一个字节
- 对于ARM Cortex-M微处理器,每个内存地址都有32位,可以寻址4GB
一个对象可能占用多个字节
- 一个字占4个字节
- 字分为小端存储和大端存储
- 以小端格式存储一个字时,最高有效字节存储在高位地址,最低有效字节存储在低地址
- 以大端格式存储一个字时,最高有效字节存储在低位地址,最低有效字节存储在高地址
指针
- 指针的值是计算机中存储的某些变量的内存地址
指针加加(假设按照小端序方式存储)
- 加1,具体是内存地址增加多少,由指针的类型决定
硬编程指针
volatile关键字
- 强制编译器每次读取新值,而不是从寄存器中读取,防止编译器在编译过程中进行错误的优化
STM32中的GPIO指针
在STM32 Cortex-M处理器的设备头文件中,外设的内存地址被强制转换为指向一个结构体
例如,GPIO端口A的存储器基地址为48000000(十六进制),使用宏指针将此地址转换为指针,该指针可以指向GPIO类型的结构体,即可通过结构体,软件可以轻松访问外设的所有寄存器
标签:int,void,深入浅出,内存,printf,include,指针 From: https://www.cnblogs.com/General-xd/p/18312149