前言
本文为4小时彻底掌握C指针的笔记。
P1 指针基本介绍
/*
前提条件:a的地址为204,p的地址为64
*/
#include "stdio.h"
int main(){
int a;
int *p;
p = &a;
a = 5;
//p带星时为值
print p; // >> 204 a的内存地址
print &a;// >> 204 a的内存地址
print *p;// >> 5 a变量中存储的数据 // 此为解引用
print &p;// >> 64 p的内存地址
}
P2 指针代码示例
#include "stdio.h"
int main(){
int n = 2;
int* p;
p = &n;
printf("%p\n",p);
printf("%p\n",p+1); //使用指针运算+1,指向了下一个该类型地址,int地址+4字节
return 0;
}
OUTPUT:
0x7fffffffe28c //此处使用的是%p
0x7fffffffe290 //此处为+4字节后的地址
整数类型
下表列出了关于标准整数类型的存储大小和值范围的细节:
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 byte | -128 到 127 或 0 到 255 |
unsigned char | 1 byte | 0 到 255 |
signed char | 1 byte | -128 到 127 |
int | 2 或 4 bytes | -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 |
unsigned int | 2 或 4 bytes | 0 到 65,535 或 0 到 4,294,967,295 |
short | 2 bytes | -32,768 到 32,767 |
unsigned short | 2 bytes | 0 到 65,535 |
long | 4 bytes | -2,147,483,648 到 2,147,483,647 |
unsigned long | 4 bytes | 0 到 4,294,967,295 |
浮点类型
下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 byte | 1.2E-38 到 3.4E+38 | 6 位小数 |
double | 8 byte | 2.3E-308 到 1.7E+308 | 15 位小数 |
long double | 10 byte | 3.4E-4932 到 1.1E+4932 | 19 位小数 |
不同系统间的差异
P3 指针的类型,算术运算,void指针
指针类型
指针属于强类型:需要用一个特定类型的指针变量来存放特定类型变量的地址。
#include "stdio.h"
int main(){
int a = 1025;
int *p;
p = &a;
printf("size of integer is %d bytes\n",sizeof(int));
printf("Address = %d,value = %d\n",p,*p);
char *p0;
p0 = (char*)p;
printf("size of char is %d bytes\n",sizeof(char));
printf("Address = %d,value = %d\n",p0,*p0);
}
#OUTPUT:
size of integer is 4 bytes
Address = -13332,value = 1025
size of char is 1 bytes
Address = -13332,value = 1
上面的代码可以看到,将指针类型强制转换为char后,输出的value = 1,原因是 1025 的二进制形式为 0100 0000 0001
,char为1字节,int为4字节,所以截取了后面的 0001
,所以输出为 1。
void指针使用
void指针无法使用解引用
*p0
,也无法使用指针运算p0+1
#include "stdio.h"
int main(){
int a = 1025;
int *p;
p = &a;
void *p0;
p0 = p; // 这里void指针是可以直接被赋值其他类型指针的
printf("Address = %d",p0;
}
P4 指向指针的指针
#include "stdio.h"
int main(){
int x = 5;
int* p = &x;
int** q = &p;
int*** r = &q;
printf("-------------\n");
printf(" x = %d\n", x);
printf(" &x = %d\n",&x);
printf("-------------\n");
printf(" p = %d\n",p); //指向x的地址
printf(" *p = %d\n",*p);
printf(" &p = %d\n",&p);
printf("-------------\n");
printf(" q = %d\n",q); //指向指针p的地址
printf(" *q = %d\n",*q);//指向x的地址
printf(" **q = %d\n",**q);//解引用x的值
printf("-------------\n");
printf(" r = %d\n",r);
printf(" *r = %d\n",*r);
printf(" **r = %d\n",**r);//指向x的地址
printf(" ***r = %d\n",***r);//解引用x的值
printf("-------------\n");
printf("由此可见,没星号的时候输出为指针自身地址\n");
printf("指针运算:r+3 = %d\n",r+3);
}
OUTPUT:
-------------
x = 5
&x = -13324
-------------
p = -13324
*p = 5
&p = -13336
-------------
q = -13336
*q = -13324
**q = 5
-------------
r = -13344
*r = -13336
**r = -13324
***r = 5
-------------
由此可见,没星号的时候输出为指针自身地址
指针运算:r+3 = -13320
***r
这么长的星号,俗称:解引用链
P5 函数传值 VS 传引用
指针作为函数参数使用,称为传引用。函数传值不会影响主函数中的变量值,传引用会影响主函数中的变量值。
Stack、Static/Global、Code(Text)这三个区段的大小在编译的时候就已经确定了,是固定的。
堆栈的方向
一个经常让人困惑的点在于,常见的文章中,堆栈经常是以相反的方向进行内存分配。如下图所示,Stack由高地址向低地址索取空间,Heap则相反从低向高。但是也有很别处是Stack自上而下,Heap相反。所以是不是写错了?到底哪种才是正确的分配方案。**其实根据系统架构的不同,这两种方式都是存在的。**只是顺序的颠倒,没有其它不同。
Bob想要实现通过一个外部函数调用来给主函数中的变量值+1,代码应该如何编写?
// 传引用代码示例
#include "stdio.h"
void Increment(int *p){
*p = (*p) + 1;
}
int main(){
int a;
a = 10;
Increment(&a);
printf("a = %d\n",a);
}
//OUTPUT:
//a = 11
P6 指针和数组
假设存在数组 A[10],A的地址为A[0]的地址,此为数组的基地址。
#include "stdio.h"
int main(){
int A[]={1,2,3,4,5};
printf("%d\n",A);
printf("%d\n",&A[0]);
printf("%d\n",A[0]);
printf("%d\n",*A);
}
OUTPUT:
-13344
-13344
1
1
P7 数组作为函数参数
#include "stdio.h"
int SumOfElement(int A[]){ //此处的int A[] 等同与 in* A
int i, sum = 0;
int size = sizeof(A)/sizeof(A[0]);
printf("Function - Size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
printf("Function arg size = %d\n",size);
for(i = 0;i<size;i++){
printf("A[%d]=%d\n",i,A[i]);
sum+= A[i];
}
return sum;
}
int main(){
int A[]={1,2,3,4,5};
int total = SumOfElement(A); //A can‘t be used for &A[0]
printf("Array SUM = %d\n",total);
printf("Main - size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
}
Function - Size of A = 8, size of A[0] = 4
Function arg size = 2
A[0]=1
A[1]=2
Array SUM = 3
Main - size of A = 20, size of A[0] = 4
由上面的代码可知,外部函数中的size值为2,int size = sizeof(A)/sizeof(A[0])
,这里的size是判断的外部函数中的数组长度,并不是Main中的数组长度。正确计算数组值的外部函数代码应为:
int SumOfElement(int A[]){
int i, sum = 0;
int size = sizeof(A)/sizeof(A[0]);
printf("Function - Size of A = %d, size of A[0] = %d\n",sizeof(A),sizeof(A[0]));
printf("Function arg size = %d\n",size);
for(i = 0;i<5;i++){ //在这里设置判断的值为主函数中的数组长度
printf("A[%d]=%d\n",i,A[i]);
sum+= A[i];
}
return sum;
}
P8&P9 指针和字符数组
C里的字符串必须以NULL结尾,也就是\0
结尾,假如存入数组的字符串为’John’,字符串长度为4,实际字符数组长度应为5,因为末尾要加上NULL。使用strlen()
函数测量字符数组长度也是以NULL作为结尾,测量出的长度等同与字符数,不会额外计算NULL。
#include "stdio.h"
int main(){
char c1[]="hello";
char* c2;
c2 = c1;
printf("代码:char* c2;c2 = c1:\n");
printf("-------------\n");
printf("c1[0] :%c\n",c1[0]);
printf("&c1[0]:%d\n",&c1[0]);
printf("c2[0] :%c\n",c2[0]);
printf("&c2[0]:%d\n",&c2[0]);
printf("-------------\n");
printf("c1:%d\n",c1);
printf("c2:%d\n",c2);
printf("-------------\n");
printf("&c1:%d\n",&c1);
printf("&c2:%d\n",&c2);
printf("-------------\n");
printf("TEST c3\n");
char c3[10];
c3[0] = c1[0];
printf("c3[0] :%c\n",c3[0]);
}
代码:char* c2;c2 = c1:
-------------
c1[0] :h
&c1[0]:-13318
c2[0] :h
&c2[0]:-13318
-------------
c1:-13318
c2:-13318
-------------
&c1:-13318
&c2:-13328
-------------
TEST c3
c3[0] :h
利用传引用
和NULL
打印字符数组:
#include "stdio.h"
void print(char* c){
int i = 0;
while(c[i] != '\0'){
printf("%c",c[i]);
i++;
}
}
int main(){
char C[20] = "hello";
print(C);
}
P10 指针和二维数组
指针的类型很重要,不是在你读地址的时候,而是再你解引用的时候,或者是你对它进行指针算术的时候。
假设存在int B[2][3]
,B
实际上是一个指向包含三个整型数据的指针,也就是12字节(一个整型数据4字节)。
int* p = B [x]
int(*p)[3] = B [√]
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针
指针数组和二维数组指针的区别
指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:
int *(p1[5]); //指针数组,可以去掉括号直接写作
int *p1[5];int (*p2)[5]; //二维数组指针,不能去掉括号
指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
int* p VS int(*p)[4]
int p:*
#include "stdio.h"
int main(){
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
int* p = a; //这里我能正确输出
printf("a:%d\n",a);
printf("a[1][1]:%d\n",a[1][1]);
printf("p:%d\n",p);
printf("*P:%d\n",*p); //这里输出的是a[0][0]的值
}
OUTPUT:
a:-13376
a[1][1]:5
p:-13376
*P:0
二维数组在内存中的存储方式:
按照正确格式int(*p)[4]输入p:
#include "stdio.h"
int main(){
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
int(*p)[4] = a; //[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针
printf("a:%d\n",a);
printf("a[1][1]:%d\n",a[1][1]);
printf("p:%d\n",p);
printf("*(p+1):%d\n",*(p+1)); // *(p+1) = p+1,指向第1行第0个元素的地址
printf("**(p+1):%d\n",**(p+1)); // **(p+1) 指向第1行第0个元素的值
printf("*(*(p)+3):%d\n",*(*(p)+3)); // *(*(p)+3) 指向第0行第3个元素的值
}
OUTPUT:
a:-13376
a[1][1]:5
p:-13376
*(p+1):-13360
**(p+1):4
*(*(p)+3):3
int(*B)[4];
B[i][j] = *(B[i]+j) = *(*(B+i)+j)
B[i]为地址,*(B+i)为地址
P11 指针和多维数组
┌───┬───┬───┐
┌───┐ ┌──▶│ 1 │ 2 │ 3 │
┌──▶│░░░│──┘ └───┴───┴───┘
│ ├───┤ ┌───┬───┬───┐
│ │░░░│─────▶│ 4 │ 5 │ 6 │
│ ├───┤ └───┴───┴───┘
│ │░░░│──┐ ┌───┬───┬───┐
┌───┐ │ └───┘ └──▶│ 7 │ 8 │ 9 │
ns ────▶│░░░│──┘ └───┴───┴───┘
├───┤ ┌───┐ ┌───┬───┐
│░░░│─────▶│░░░│─────▶│10 │11 │
├───┤ ├───┤ └───┴───┘
│░░░│──┐ │░░░│──┐ ┌───┬───┐
└───┘ │ └───┘ └──▶│12 │13 │
│ └───┴───┘
│ ┌───┐ ┌───┬───┬───┐
└──▶│░░░│─────▶│14 │15 │16 │
├───┤ └───┴───┴───┘
│░░░│──┐ ┌───┬───┐
└───┘ └──▶│17 │18 │
└───┴───┘
如果我们要访问三维数组的某个元素,例如,ns[2][0][1]
,只需要顺着定位找到对应的最终元素15
即可。
多维数组跟二维数组差不多,几维就几个星才能取到值。
方括号等于一星。
三维数组的指针传入:
voidFunc(int(*A)[2][2]){
}
P12 指针和动态内存 - 栈 VS 堆
堆:空闲的内存池,动态内存
静态内存是在栈上分配的,而动态内存是在堆上分配的。
为了在C中使用动态内存,我们需要知道4个函数:malloc
calloc
realloc
free
[ 这四个函数也可在C++中使用 ]
为了在C++中使用动态内存,我们需要知道4个函数:new
delete
malloc
作用:从堆上找到空闲内存,为你预留空间然后通过void指针返回给你。如果无法找到空闲内存,malloc会返回NULL。
void指针一般被称为通用指针或叫泛指针。 它是C语言关于纯粹地址的一种约定。 当某个指针是void型指针时,所指向的对象不属于任何类型。 因为void指针不属于任何类型,则不可以对其进行算术运算,比如自增,编译器不知道其自增需要增加多少。
free
作用:使用malloc分配的内存,最终通过调用free进行释放。
int *p;
p = (int*)malloc(sizeof(int));
p = (int*)malloc(20*sizeof(int)); //分配数组
free(p);
new 和 delete
int *p;
p = new int;
delete p;
p = new int[20]; //分配数组
delete[] p;
P13 malloc,calloc,realloc,free
malloc
malloc - void* malloc(size_t size)
size_t是一种无符号的整型数,它的取值没有负数,在数组中也用不到负数,而它的取值范围是整型数的双倍。 sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。 该类型保证能容纳实现所建立的最大对象的字节大小。
calloc
calloc - void* calloc(size_t num,size_t size)
示例:
int* p = (int*)calloc(3,sizeof(int))
calloc返回void指针,但是calloc接收2个参数:
- 特定类型的元素的数量
- 类型的大小
malloc与calloc的区别:
- malloc 参数只有一个,calloc参数有两个
- malloc 分配完内存后不会对其进行初始化,因此如果没有填入值的话就会得到一些随机值(垃圾数据),calloc会对其进行初始化
realloc
realloc - void* realloc(void* ptr,size_t size)
realloc接收两个参数:
- 第一个参数是指向已分配内存的起始地址的指针
- 第二个参数是新的内存块的大小
realloc会把已分配内存块的内容拷贝过去 [ 感觉这个函数没啥用 ]
free
函数free的入参是内存的起始地址。
应用场景
假设用户想要输入一个变量n,并且创建一个数组为A[n],如何做?
- 直接使用
int A[n]
: 会报错,因为静态内存需要明确指出数组的大小。 - 正确做法:
int* A = (int*)malloc(n*sizeof(int))
P14 指针和动态内存 - 内存泄漏
内存泄漏只会由于Heap引起。内存泄漏指的是我们动态申请了内存,但是即使是使用完了之后也从来都不去释放它。
栈的内存是自动回收的,栈帧结束后会自动释放,栈的大小是固定的。
P15 函数返回指针:return 地址
#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);
}
上面这个程序无法成功运行,你知道为什么吗?
因为函数返回指针时,该函数在栈里已经被释放掉了。
要使用函数返回指针,需要在heap
中使用。因为heap的内存释放需要手动释放。
P16 函数指针
函数实际上是内存上的一块连续地址,函数指针指向起始地址。
#include "stdio.h"
#include "stdlib.h"
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int c;
int (*p)(int, int); //函数指针
p = &Add;
c = (*p)(2, 3); //这行代码也可写成 c = p(2,3);
printf("%d\n", c);
}
P17 函数指针的使用案例:回调函数
#include "stdio.h"
void A(){
printf("hello\n");
}
void B(void (*ptr)()){
ptr();
}
int main(){
void (*p)() = A;
B(p);
}
其中,void (*p)() = A; B(p);
可简写为B(A);
,函数A为回调函数。