长期的一致性,胜过短期的高强度。|

在下阿shen

园龄:3年6个月粉丝:0关注:5

2023-11-13 16:37阅读: 53评论: 0推荐: 0

《4小时彻底掌握C指针》笔记

学习的视频地址在B站:《4小时彻底掌握C指针》 由fengmuzi2003大佬翻译制作。感谢!

这篇博客是借由这个视频对C语言的一次梳理。有错误的话欢迎指正。

ch1.Introduction to pointers in C

先从简单的变量看起。

img

在先编译器中一般会为int型变量分配4个字节,char型变量分配1个字节,浮点型变量分配4个字节(1 Byte = 8 Bit)。现在声明int型变量a,char型变量c:

int a; // 204
char c; // 209

那么此时a的所占的内存大小是4,地址204~207的内存单元都被a所占用,如果是小端法那么变量a的地址就是204。同理,char型变量是209。那么语句a++代表将204地址的int型数据增加1。

1.1 什么是指针

Pointers - variables that store address to another variable

img

上图右侧的内存结构显示,在地址204处有我们的int型变量a,a的值是8。在内存64地址中,存放着一个int*指针变量P,P中存放的是a的地址信息。*P代表解引用(dereferencing),表示对该指针对应的值。伪代码Print *P 则输出的是5,假如对其进行操作*P = 8那么输出的a的值则变为8。当然,对指针变量P进行取地址输出的是地址信息64,因为指针也是一个变量,存放在内存中也是有地址的。

1.2 指针变量的大小

在上图中,注意下指针变量P的大小是4字节(假如是32位机器),但不是因为是int型,如果有一个指针变量char *Q,那么Q的大小仍然是4字节。

指针变量代表内存中的地址。

  • 在32位的计算机中,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

  • 在64位的计算机中,如果有64个地址线,那一个指针变量的大小是8个字节,才能放下一个地址。

所以,指针的大小在32位平台上是4个字节,在64位平台上是8个字节

ch2.Working with Pointers

这部分来写一下简单的代码。

2.1 解引用的赋值

#include <stdio.h>
int main()
{
int a;
int *p;
a = 10;
p = &a;
printf("Address of P is %d\n", p);
printf("Value of P is %d\n", *p);
int b = 20;
*p = b; // Will the address in p change to point b ?
printf("Address of P is %d\n", p);
printf("Value of P is %d\n", *p);
return 0;
}

在第13行代码执行后,变量的p的地址会编程指针b的吗?不是的,解引用相当于是将指向的变量放到当前情况下,这句话实际上是将b的值赋给变量a。

2.2 指针变量

#include <stdio.h>
int main()
{
char a = 'A';
char *p = &a;
// Pointer arithmetic
printf("%d\n", p); // 6422039
printf("Value of P is %c\n", *p); // A
printf("Size of integer is %d bytes \n", sizeof(char)); // 1
printf("%d\n", p + 1); // 6422040
return 0;
}
#include <stdio.h>
int main()
{
int a = 10;
int *p = &a;
// Pointer arithmetic
printf("%d\n", p); // 6422036
printf("Value of P is %d\n", *p); // 10
printf("Size of integer is %d bytes \n", sizeof(int)); // 4
printf("%d\n", p + 1); // 6422040
return 0;
}

对指针变量进行加1操作,int型地址是加4,char加1,都向后移动一个类型的大小。通过指针可以访问内存中任何位置,而移动后的位置在内存中是没有初始化的,所以可能是随机值,很危险。

ch3.Pointer types,void pointer,pointer arithmetic

指针是强类型的,对于一个int*就需要一个指向整型类型的指针来存放整型数据的地址,如果是字符型的变量就需要字符型的指针来存放变量地址,如果是自定义结构那就需要这个类型的指针来指向。因为我们不仅使用指针来存储内存地址,而且也使用它来解引用这些地址的内容。

img

如上图所示,对于一个整型变量a(见图片右侧中间部分),当赋值1025时,其对应的四个字节200-203存储的二进制数字分别为00000000 00000000 00000100 00000001,如图中标注(小端,字幕标注为LSB)。考虑如下代码:

int a = 1025;
int* p = &a;
char* p0;
p0 = (char*)p; // typecasting
printf("%d, %d\n", p, *p); // 200, 1025
printf("%d, %d\n", p+1, *(p+1)); // 204, -2454153
printf("%d, %d\n", p0, *p0); // 200, 1
printf("%d, %d\n", p0+1, *(p0+1)); // 201, 4
// 1025= 00000000 00000000 00000100 00000001

代码第4行,(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强转成char,然后再对char进行加法操作,则会遍历int的内存数据,这时再进行char解引用就会出现有意思的现象。

3.1 通用类型指针void*

int a = 1025;
void* p1;
p1 = &a;
printf("%d\n", p1); //200
// printf("%d\n",*p1);
// printf("%d\n",p1+1);

如上代码所示,无需类型转换,可以将任意类型变量的地址赋值给void*类型指针,但是该类型指针只能打印地址,无法使用解引用*以及+1等运算操作。

所以指针的加法操作是对类型有强要求的,必须得先确定指针的类型才可以。

ch4.Pointer to pointer

这节课感觉听小哥的建议多写点代码玩一下会理解更好。

img

这里也意识到**p写成*(*p)这样会更方便阅读。下边是根据理解写的一段小代码:

#include <stdio.h>
int main()
{
int a = 10;
int *a1 = &a;
int **p = &a1;
double d = 10.5;
double *d1 = &d;
char c = 'A';
char *c1 = &c;
printf("Value of the a %d \n", a );
printf("Value of the a1 %d \n", a1 );
printf("Value of the p %d \n", p );
printf("Value of the *p %d \n", *p );
printf("Value of the **p %d \n", **p );
printf("Size of the a1 %d \n", sizeof(a1) ); //8
printf("Size of the p %d \n", sizeof(p) ); //8
printf("Size of the *p %d \n", sizeof(*p) ); //8
printf("Size of the **p %d \n", sizeof(*(*p)) ); // 4 和**p一样,加括号为了可读性更好
printf("Value of the *d1 %f \n", *d1 ); //10.500000
printf("Size of the d1 %d \n", sizeof(d1) ); // 8
printf("Value of the *c1 %c \n", *c1 ); //A
printf("Size of the c1 %d \n", sizeof(c1) ); // 8
return 0;
}

可以看到如果对指针进行sizeof操作,不管是什么类型的,指针所占的空间都是8字节(64位机器)。这是这小结收获比较大的地方。

ch5.Pointers as function arguments - Call by reference

#include<stdio.h>
void Increment(int a){
a = a+1;
printf("%d\n", &a);
}
int main(){
int a;
a = 10;
Increment(a);
printf("a=%d\n", a);
printf("%d\n", &a);
}
//~output~
//6422000
//a=10
//6422044

首先以上面代码为例解释函数按值传递。在main函数中,我们定义了变量a,并试图调用Increment函数使a增加1。但是当我们测试该代码时发现,a的值并没有如愿+1=11。我们同时打印了a在main函数和Increment函数中的地址,我们发现两者地址是不一样的,即两个函数中的a并不是同一个a,因此在main函数中的a并没有+1

img

在这里小哥将,内存模型分为四个部分:

  • 堆区
  • 栈区
  • 静态及全局变量区
  • 代码段

这节重点是对栈区的讲解,将Increment函数中的变量改成指针方式,那么传入函数中的参数是外部变量的地址,而不是在函数内的局部变量。在函数内的修改会直接作用在目标地址上,所以函数结束后,才能将结果准确的返回给调用者。

另外,在下边的参考博客中,我也搜索了一下相关博客,发现c或者c++对内存管理是更细致的,还需要深入理解。

ch6.Pointers and Arrays

img

这节主要熟悉下C语言中数组的使用方式以及和指针的关系。在C语言中数组声明的时候需要给定大小,数组名代表着块连续区域的地址。如上图所示,对于元素的访问可以总结如下:

  • Address - &A[i] or (A + i)
  • Value - A[i] or *(A + i)

在C语言中,将数组理解成地址序列比较好,通过指针这个角度来进行变成可能会更自由,而且能够尽量规避错误。在解决一些问题的时候可能会有更多的灵感,比方说,数组动态增长,数组拷贝,数组遍历等。

ch7.Arrays as function arguments

这一小结是ch5的拓展,把数组当参数传进函数的时候,一般是引用传递,因为数组名也是一种地址信息当作指针来看的嘛。不过这个地址的只有头的信息,没办法知道结尾,也就是说传数组进去后,我们需要有个标志来判断数组的结尾,比方说传完数组名后,再传进去一个数组长度信息,这样这个函数获取到完整的数组信息了。

不过要注意,不能使用 sizeof(a) / sizeof(int);数组长除以元素大小,比方像下边这样,因为我是64位机器,所以sizeof(a)也就是指针的大小是8个字节,而sizeof(int)是4个字节,所以在第6行代码计算出来的数组长度是2,SumOfElements函数返回的也就是前两个元素相加为5了。

#include <stdio.h>
int SumOfElements(int a[], int size)
{
int sum = 0;
int sizeOfCount = sizeof(a) / sizeof(int);
printf("a[] %d\n",sizeof(a) );
printf("sizeof(int) %d\n",sizeof(int) );
printf("sizeOfCount is %d\n", sizeOfCount);
for (int i = 0; i < sizeOfCount; i++)
{
sum += a[i];
}
return sum;
}
int main()
{
int a[] = {2, 3, 67, 4, 9};
printf("Sum of Elements is %d\n", SumOfElements(a, 5));
}

所以使用传进去的实际数组长度就可以进行正确计算了。另外,对数组的数值操作也会将保留在原始数组上,比如下边的将数组元素翻倍的程序:

#include <stdio.h>
void Double(int a[], int size)
{
for (int i = 0; i < size; i++)
{
a[i] = a[i] * 2;
}
}
int main()
{
int a[] = {2, 3, 67, 4, 9};
Double(a, 5);
for (int i = 0; i < 5; i++)
{
printf("%d ", a[i]);
}
}
//~output~
//4 6 134 8 18

ch8.Character arrays and pointers Ⅰ

在C中没有提供String这样的类型,想使用字符串,就需要使用char的数组。C语言提供String.h库来处理一些字符串操作,参数类型一般就是char *

而字符串默认有个NUL或者\0的结束符,看一下下边没有结束符会出现什么现象:

#include <stdio.h>
int main()
{
char C[4];
C[0] = 'J';
C[1] = 'H';
C[2] = 'O';
C[3] = 'N';
printf("%s", C);
}
//~output~
//JHON�

在正确输出了前四个字符后,又输出了一些乱码,所以在C中使用字符串要常常先考虑当前字符串该如何结束

另外,视频中还提到字符串的赋值。比方有一个静态字符串C1和一个字符指针变量C2:

char C1[] = "Hello";
char *C2;

那么C1可以赋值给C2,但是C2不能赋值给C1:

C2 = C1; // yes
C1 = C2; // error

同样的C1也不能进行自增

C1++; // error

数组名是一种静态指针,有比较严格的限制。刚刚在博客看到一个比较好的描述——有数组名的地方可以替换成指针,但反过来不行。

ch9.Character arrays and pointers Ⅱ

img

这一小节主要是收获了数组的使用时的内存模型。当使用char C[20] = "..."; 来声明字符串时数据是存放在栈空间中的,这意味着可以数据内容可以被更改:

char C[20] = "Hello";
C[0] = 'A';
print(C); //"Aello"

而如果使用char *C = "Hello";来声明字符串变量,数据是存放在静态区域的,如果尝试去修改就会产生错误:

char *C = "Hello";
C[0] = 'A'; // Error!

ch10&11.Pointers and multi-dimensional arrays

img

这小节比较重要,主要讲C语言中的二维数组。二维数组可以写成int B[2][3];的形式,代表是两个含有三个元素的数组。可以用int (*p)[3] = B;这样的指针变量指向这个数组名(当然反过来不行)。以上图的地址和对应值为例,二维数组和指针的关系:

Print B or &B[0] // 400
Print *B or B[0] or &B[0][0] // 400

在第二行,对B进行了一次解引用,得到了第一个数组的数组名,所以输出的值是地址400。我们再继续:

Print B+1 or &B[1] // 412
Print *(B+1) or B[1] or &B[1][0] // 412
Print *(B+1)+2 or B[1]+2 or &B[1][2] // 420
Print *(*B+1) // 3

在第四行,先对B取地址得到第一个数组的数组名,将数组名加1,得到第一个数组的第二个元素的地址(略绕),再对这个地址进行解引用,得到该元素的值。

所以对二维数组总结如下:

  • B[i][j] = *(B[i]+j) = *(*(B+i)+j)

进而理解三维数组:

img

看图的话就是三个二维数组,二维数组是两个含有两个元素的一维数组。三维数组也有着上边的总结:

  • B[i][j][k] = *(B[i][j]+k) = *(*(B[i]+j)+k) = *(*(*(B+i)+j)+k)

快速想一下:

Print *(C[1]+1) // ?

首先C[1]是第二个二维数组名,数组名+1代表的是这个二维数组中第二个一维数组数组名的位置(还没有获取到数组名?),所以对这个位置进行解引用得到了这个数组名的地址824。

写个程序搞一下:

#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\n", C, *C, C[0], &C[0][0]);
printf("%d\n", *(C[0][0] + 1));
printf("%d\n", (C[1] + 1));
printf("%d\n", *(C[1] + 1));
printf("%d\n", **(C[1] + 1));
}
//~output~
6422000 6422000 6422000 6422000
5
6422024
6422024
6

代码的实际运行结果是,*(C[1] + 1))(C[1] + 1))是一样的,都是数组的基地址。

printf("%d\n", (C[1] + 1)+2);
printf("%d\n", **((C[1] + 1)+2));
printf("%d\n", *(*((C[1] + 1)+2)+1));
//~output~
6422040
11
13

上边这段代码像是将这个三维数组从第二个二维数组的第二个一维数组截开,以此为开始进行向后遍历,这里的+2就是对应【11,13】这个一维数组。立刻觉得指针好灵活啊!不过这样的话不容易理解,可读性就差了很多。

本节最后,还提到将多维数组当作参数传入函数:

void Func(int (*A)[2][2]); //传入三维数组
void Func(int A[][3]); //传入二维数组

除了第一个维度可以不指定外,后边的维度都是强制需要补全的,这点要注意。

另外,不能像一维数组那样直接传入三维数组的地址:

void Func(int ***A); // error

ch12.Pointers and dynamic memory Ⅰ

img

这个小节重点讲了栈和堆的区别。他们都是数据结构,区别在于栈区是在程序运行时就已经固定的,随着入栈数据变多,造成栈区使用耗尽,会产生栈溢出(stack overflow)的问题。而堆区可以在程序运行时动态进行申请与释放,控制函数有malloc,calloc,realloc,free等函数。

#include<stdio.h>
#include<stdlib.h>
int main(){
int a;
int* p;
p = (int*)malloc(sizeof(int));
*p = 10;
free(p);
p = (int*)(20*sizeof(int));
free(p);
}

ch13.Pointers and dynamic memory Ⅱ

malloc 函数的使用如下所示。这里提示直接写想要申请的字节数量并不是一个很好的习惯:

int* p = (int*)malloc(3 * sizeof(int));

calloc函数的使用如下所示,需要输入两个参数,分别为数量和单个类型大小。

int* p = (int*)calloc(3, sizeof(int));

malloc不同的是,calloc会把申请的内存全部初始化为0,而通过malloc申请的内存并不会初始化:

realloc的函数如下所示,表示重新分配指针p(p须指向堆区内存)所指堆区内存的大小,如果申请新的内存能在原有基础上找到足够大小的连续内存,则会在原位置扩展,如果申请的内存过大,则将已有内存内存储的数值一起拷贝至一个新的内存位置。重新分配完内存后,此时再次使用p是危险的。
代码第二行,如申请的内存大小为0,与free等价;代码第三行,如果传入指针为NULL,则与malloc等价。

int* p1 = (int*)realloc(p, 3 * sizeof(int));
int* p2 = (int*)realloc(p1, 0); // free(p1)
int* p3 = (int*)realloc(NULL, sizeof(int)); // (int*)malloc(sizeof(int))

强调,当通过指针p释放掉(free(p))堆区内存后,还可以通过指针p访问该段内存,但这是十分危险的。

ch14.What is memory leak?

#include<stdio.h>
#include<stdlib.h>
void Play(){
char* C=(char*)malloc(3 * sizeof(char))
}
int main(){
while (1) {
Play();
}
}

在运行代码时会发现,程序所占用内存随时间在急速增加,原因则是未使用free函数释放堆区内存。当调用函数Play时,在堆区申请3个字节的内存,并使用指针p指向该段内存,当Play函数执行完毕返回至 main时,Play函数中的局部变量将被清除,但是堆区申请的内存并不会自动释放掉,也无法被再次使用,因此随着循环的执行,将有越来越多的堆区内存没有被释放,从而造成了内存泄漏。

注意内存泄漏并不是指针C未释放,C是在栈区在Play函数结束后会自动释放,造成内存泄漏的是指针C指向的堆区内存没有被释放

ch15.Pointers as function returns

img

如何实现返回指针的需求呢?可以考虑返回全局区或者堆区的指针,因为这些区域的内存并不会被系统自动释放掉。代码如下:

#include<stdio.h>
#include<stdlib.h>
int* Add(int* a, int* b) {
int* c = (int*)malloc(sizeof(int));
*c = (*a) + (*b);
return c;
}
int main() {
int a = 2, b = 4;
int* ptr = Add(&a, &b);
printf("%d\n", *ptr);
free(ptr);
}
//~output~
6

不要返回局部变量(栈区)的指针(地址),因为栈区不稳定,会被操作系统自动释放掉。

ch16.Function Pointers

Pointers CAN:

  • point to data
  • point to function

指针指向的是内存地址,而非必然是变量。指针也可以指向函数。

img

何为函数指针?如上图所示,当写完源代码如program.c后,编译器(compiler)最终会把编译成program.exe的可执行文件,文件内容为二进制的机器码。程序在调用函数时,会跳转(call指令)至代码区函数所在的起始位置,并逐行执行函数的机器码。因此函数在内存模型中也可以认为有地址的。

如何使用函数指针呢?如下代码所示:

#include<stdio.h>
int Add(int a, int b) {
return a + b;
}
int main() {
int c;
int (*p)(int, int); // int* p(int, int)
p = &Add; // p = Add
c = (*p)(2, 3); // c = p(2, 3)
printf("%d\n", c);
}

代码中,第七行定义了函数指针p,注意其与注释部分代码的区分,注释部分表示返回值为int的函数的声明。代码第八行将函数Add赋值给指针p,其中符号&可以省略,如注释部分所示;代码第九行则通过指针p调用了Add函数,其中符号可以省略,如注释部分所示。

更多的时候我们使用typedef来使用函数指针,如下:

typedef int (*ADD)(const int&, const int&);
ADD padd = Add;

ch17.Function pointers and Callbacks

实际的函数指针用例——回调函数

先通过代码有个直观印象:

/* 简单的回调函数 */
#include <stdio.h>
void A()
{
printf("Hello");
}
void B(void (*ptr)())
{
ptr();
}
int main()
{
void (*p)() = A;
B(p); // B(A)
}

函数B的参数是一个函数指针,在调用的时候将声明好的A传入到B函数中,这样实际是调用的A函数。

下边通过冒泡排序看一下,回调函数的使用:

/* 通过冒泡排序的设置 */
#include <stdio.h>
#include <stdlib.h>
int compare(int a, int b) {
if (a > b) return -1;
else return 1;
}
int abosulte_compare(int a, int b) {
if (abs(a) > abs(b)) return -1;
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) {
temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
}
}
}
}
int main() {
int i, A[] = { -31,22,-1,50,-6,4 };
BubbleSort(A, 6, compare);
for (i = 0; i < 6; i++) { printf("%d ", A[i]); }
printf("\n");
BubbleSort(A, 6, abosulte_compare);
for (i = 0; i < 6; i++) { printf("%d ", A[i]); }
}
//输出
// 50 22 4 -1 -6 -31
// 50 -31 22 -6 4 -1

这样调用的有点是可以修改函数内部的关键步骤,对于冒泡排序就是这个比较方式。回调函数在底层使用的很多,想操作系统的一些响应事件,需要根据文档自己补充声明一些已经定义好的函数,并通过API注册到操作系统中。另外还有GUI的一些程序也会用到。在视频的结尾小哥提到回调函数被广泛用于事件处理这样的情景中。

参考博客

LSB

C++ 中的谓词

C/C++中内存管理

C语言的数组名

本文作者:在下阿shen

本文链接:https://www.cnblogs.com/shen97/p/17829479.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   在下阿shen  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起