【学习笔记】《深入理解C指针》

前言

作者:[美] Richard Reese
出版社:人民邮电出版社
出版日期:2014年

很多都是自己的理解,如果有不准确的地方烦请指出来!
里面的绝大部分代码是从书上搬过来的,而且没有编译过,只是想学习思想

第1章 认识指针

指针实际上就是保存地址的变量

内存分类特点
全局内存
静态内存只在声明它的函数内部能访问,但是内存在程序开始使分配,直到程序终止才消失
局部内存在函数被调用时才创建,函数结束就消失
动态内存内存分配在堆上,直到释放才消失

我以前总有种感觉int a;是一个变量,那么int *a;就是指向a的指针。
但是实际上略有区别, 我们不如把它写作int* a;这样更容易理解a是一个指向int的指针变量(就是和整型变量、浮点型变量同级的感觉),我们可以给它赋值int num = 5; a = #
但是很容易错的一点是int* a1, a2;这样定义的话只有a1是指针,必须要int *a1, *a2;这样写才行,所以一般情况下我还是会把*紧挨着变量名写。但是每个变量声明独占一行更好

然后很容易混淆的一点是如果你写printf("%d\n", *a);实际上这里的*int*有所不同,我们可以叫它解引操作符,我个人的理解是相当于解除引用,又顺着指针变量a里面存的地址找回来,输出的就是5。

指向常量的指针,例如const int* a = #,表明不可以改变指针指向的那个值,但是可以改变指针来指向另一个值。

还有一些目前不常用的知识我就没写,等到用到的时候再写。因为我一开始看第一章的时候一点都看不懂,感觉又杂又乱而且很难理解,但是看到后面再往前回顾的时候有的就茅塞顿开了,所以把一些知识挪到后面应用的时候一起说我觉得会清楚一点。

第2章 C的动态内存管理

栈和堆咋讲呢。我的理解就是,每次调用函数会在栈上push进去一个栈帧(就有点像是普通的栈里面push进一个整数,只不过栈帧表示一块内存),函数return之后再pop出去。堆就是如果你用malloc等函数申请内存的话,就会存在堆上,就好像露宿街头居无定所,很“动态”你知道吧【狗头】。随着内存的分配和释放,堆中会布满碎片【创建你,给你分配内存,用你,用完了释放你,把你丢在大街上】。然后栈和堆处在同一个数据段中,因此存在相互覆盖的可能,全局变量和静态变量存在不同的数据段中。

刚才提到了用malloc函数从堆上分配一块内存,它会返回首字节的地址。
函数的原型:void* malloc(size_t);【size_t类型是sizeof函数的返回值,是无符号整数,我觉得知道这些就够了,看C reference的时候啥都不懂不也一样看】
【void* 是通用指针类型,可以存放任意数据类型的引用,任何指针都可以被赋给void指针,但是void指针转换成其他类型的指针需要类型转换(如下面malloc示范那行代码)。这个东西我觉得自己写题的时候很少会用到,只是知道它是怎么回事就够了】
int *pi = (int*) malloc (size * sizeof(int));size就是你想开的大小,sizeof(int)就是4字节。
要注意当malloc无法分配内存的时候会返回null,用之前判断一下是个好习惯。

分配完了用完了之后需要用free函数释放内存。free(pi);
函数的原型:void free(void *ptr);参数应该是由malloc类函数【这里是说malloc那一类的函数,包括calloc、realloc】分配的内存的地址,这块内存会被返还给堆。用malloc好比你用手抓了一个空箱子用来放东西,free了之后就相当于你把手放开,但是箱子还在,手也还在,只是之间的连接断开了。如果你还顺着手找它拿着的箱子,这个时候就找不到有效对象,称你的手为迷途指针,你这种行为就是无法预期的(手松开箱子之后说不定空气中会有什么奇奇怪怪的东西到你手里了)。
【解决方法书里讲的不是太明白,等我懂了再来更新好了】

如果你分配了内存之后却没有回收,就会导致内存泄漏。一种情况是你没有free,另一种情况是你把指针赋值为一个新地址,那它原来存着的地址就找不到了,丢了。

第3章 指针和函数

用指针传递数据可以让函数得以修改数据
大家来找茬:

int* allocateArray(int, size, int value)
{
	int arr[size];
	for (int i = 0; i < size; i++)
	{
		arr[i] = value;
	}
	return arr;
}

int* vector = allocateArray(5, 45);
for (int i = 0; i < 5; i++)
{
	printf("%d\n", vector[i]);
}

函数返回后,数组地址是无效的,你只能得到一个指针。因为数组是开在栈里面的,函数的栈帧弹出后就带着数组跑了。
但是可以这样写int* arr = (int*)malloc(size * sizeof(int));只替换函数的第一行。因为malloc内存是分配在堆上的,不会随栈帧弹出。

如果要修改原指针,而不是指针指向的地址,就需要用到指针的指针。上面那个分配内存的函数,如果不想让它返回指针的话,可以这样写:

void allocateArray(int **arr, int size, int value)
{
	*arr = (int*)malloc(size * sizeof(int));
	if (*arr != NULL) //在使用指针之前判断它是否为空是个好习惯!
	{
		for (int i = 0; i < size; i++)
		{
			*(*arr + i) = value; //这里是数组的指针表示法,下一章细说
		}
	}
}

int* vector = NULL;
allocateArray(&vector, 5, 45);

调用的时候第一个参数是指针的指针,传值,就需要传给它指针的地址。
如果这里不传递指针的指针的话,如果是int* arr,那么arr只是vector的一个副本,改变它和vector没有一点关系。

实现自己的free函数这里我不是很懂。free完了不是会产生迷途指针嘛,但是粗暴地直接赋给它NULL又可能出现问题,除了初始化,都不能将NULL赋给指针(这里不知道原因)。于是写一个函数传入要释放的指针的地址,然后free它的指针,再将它的指针赋为NULL,即

void safeFree(void **pp)
{
	if (pp != NULL && *pp != NULL)
	{
		free(*pp);
		*pp = NULL;
	}
}

#define safeFree(p) safeFree((void**)&(p)) //使用void类型可以传入所有类型的指针,但是调用时需要显式的类型转换

后面还有一部分函数指针的内容,我大体看明白了,但是目前还觉得没有太大用,以后如果很有用的话再来写叭
我来了,在第5章和第6章用到了函数指针,把详细说明和实例结合起来写了一下。

以前不知道sort的第三个参数为什么直接把比较函数的名字传进来就行,现在知道是传的函数的地址(和数组类似)。

bool cmp(pair<int, int> a, pair<int, int> b)
{
	return a.first > b.first;
}

sort(s.begin(), s.end(), cmp);

在第5章写到了自己实现的一个sort函数,用的就是函数指针。

第4章 指针和数组

数组是能用索引访问的同质元素的连续集合。这里的连续是指数组的元素在内存中是相邻的。多维数组按照方括号从右向左的顺序连续分配内存。
数组有两种表示方法,第一种是用方括号的数组表示法pv[i],第二种是指针表示法*(pv + i)。两种表示方法等价,但生成的机器码不同。pv[i]表示从位置pv开始,移动i个位置,*(pv + i)表示从pv开始,在地址上增加i,再取出这个地址中的内容。

int vector[5] = {1,2,3,4,5};
int *pv = vector;

单独使用数组名字时会返回数组地址,即&vector[0]vector等价,但&vector[0]是一个整数指针,vector是一个整数数组指针,类型不同。
像vector这样的数组名字不是左值,我们不能修改数组所持有的地址。

向函数中传递数组,一维的可以用int a[]int *a
而传递多维数组时,如果想要在函数内部使用方括号,就必须指定数组的形态,即每一维的大小。例如void display_2Darray(int a[][5], int rows)void display_2Darray(int (*arr)[5], int rows),注意不能写成int *arr[5],会被当成整数指针型的数组。
也可以这样写:

void display_2Darray_unknownsize(int *arr, int rows, int cols)
{
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			printf("%d ", *(arr + (i * cols) + j));
		}
		printf("\n");
	}
}

只不过无法使用下标arr[i][j],因为编译器不知道列数无法计算偏移量。

第5章 指针和字符串

字符串字面量就是一段用双引号括起来的字符序列(不要和单引号引起来的字符字面量搞混),它存在字面量池中,多次引用时通常只有一份副本。但为了防止被改变,也可以设成常量。
char array[] = "chapter";这样初始化时会在堆上分配内存
char *array = "chapter";则会直接指向字符串字面量

比较字符串
int strcmp(const char *s1, chost char *s2);s1 < s2返回负数,相等0,>正数

char s[16];
if (s = "Quit"{
    ...

先不说一个等号是赋值不是比较,也无法把字符串字面量的地址赋给数组名字。

if (s == "Quit") { ...

这样也是不行的,我们总会得到假,因为比较的是数组s的地址和字符串字面量的地址。

复制字符串
char* strcpy(char *s1, const char *s2);
在给s1分配内存的时候,需要strlen(s2) + 1,留给最后的’\0’

拼接字符串
char *strcat(char *s1, const char *s2);
此函数把s2拼接到s1的结尾,但是函数不会分配内存,所以s1要足够长。函数返回值的地址和第一个参数的地址一样,可以作为printf的返回值。

snprintf函数:前两个参数是要写入的字符串地址和大小,后面的参数和printf一样。

char *buffer;
size_t size;
snprintf(buffer, size, "Item: %s", name);

写一个函数将传递进来的字符串用小写形式返回

char* stringTolower(const char* string)
{
	char *tmp = (char*)malloc(strlen(string) + 1);
	char *start = tmp;
	while (*string != 0)
	{
		*tmp = tolower(*string);
		string++;
		tmp++;
	}
	*tmp = 0;
	return start;
}

这段代码好有意思,首先给tmp申请内存的时候长度要+1,是留给字符串最后的‘\0’的,机器如果读不到它就会不认这是个字符串,就会出现乱码。
接下来第二行,tmp表示申请到的内存头上的地址嘛,就把这个地址赋给start指针,保存下来字符数组首个元素的地址。
第三行while语句,这个0其实不是数字,它代表的是ASCII码第0个,也就是‘\0’。
倒数第二行给*tmp赋值0,不是要把它置空,而是在字符串的末尾手动加上’\0’。
最后return start是return的已经变成小写的字符串的头指针。妙啊。
这个函数返回的是动态分配内存的指针,这意味着一旦不需要就应该将其释放掉。

int compareIgnoreCase(const char *s1, const char *s2)
{
	char *t1 = stringToLower(s1);
	char *t2 = stringToLower(s2);
	int result = strcmp(t1, t2);
	free(t1);
	free(t2);
	return result;

接下来实现一个给予冒泡排序的sort函数,将数组地址、长度以及一个控制排序的函数指针传递进去。

// 先使用类型定义声明要用的函数指针,参数是两个char常量指针,返回int
typedef int (*fptrOperation)(const char*, const char*);

void sort(char *array[], int size, fptrOperation operation)
{
	int swap = 1;
	while(swap)
	{
		swap = 0;
		for (int i = 0; i < size; i++)
		{
			if(operation(array[i], array[i + 1] > 0)
			{
				swap = 1;
				char *tmp = array[i];
				array[i] = array[i + 1];
				array[i + 1] = tmp;
			}
		}
	}
}

char *name[] = {"Bob", "Ted", "Carol", "Alice", "alice"};
sort(names, 5, compareIgnoreCase);

我以前老是忘记stl里面自带的sort函数怎么自定义,现在学了函数指针之后明白了它第三个参数就是自己写一个函数传进去,应该是库里面给实现了函数指针。

字符串的输入scanf("%s", names);是不需要加&的,但是字符scanf("%c", &s);是需要加的,我以前一直是死记,就老忘,总是需要写个小程序验证一下。现在知道了是因为names是一个数组的名字,写它就相当于写地址。

第6章 指针和结构体

为结构体分配内存时,结构体各字段之间可能会有填充,比如说整数对齐到能被4整除的地址上。这就意味着一般情况下不能使用指针算术运算(应该支队数组使用指针算术运算,因为数组肯定分配在连续的内存块上),以及结构体数组可能需要额外的内存。

在C中使用结构体需要手动写initialize和deallocate函数来初始化和释放内存,但是书上说C++这类面向对象的编程语言会自动为对象调用这些操作,所以我应该会学一学c++里面要怎么办再来更新。

重复分配然后释放结构体会产生一些开销,解决方法之一是为他们维护一个结构体池,用完了就丢进里面,需要的时候从里面取,如果都去完了就动态分配一个。这个思想可以用在其他地方。

下面我们用指针来实现一个单链表

typedef struct __empolyee {
	char name[32];
	unsigned int age;
} Employee;

typedef struct _node {
	void *data; // 使用void指针可以持有任意类型的数据
	struct _node *next;
} Node;

typedef struct _linkedList {
	Node *head;
	Node *tail;
	Node *current; // 用来辅助遍历链表
} LinkedList;

void initializeList(LinkedList *list)
{
	list->heat = NULL;
	list->tail = NULL;
	list->current = NULL;
	// 这里不能像int或bool赋值一样写连等,因为他们将来会指向不同的地方
	// 但是我不是很确定_(:з」∠)_
}

void addHead(LinkedList *list, void *data)
{
	Node *node = (Node*)malloc(sizeof(Node));
	node->data = data;
	if (list->head == NULL)
	{
		list->head = node;
		node->next = NULL;
	}
	else
	{
		node->next = list->head;
	}
	list->head = node;
}

void addTail(LinkedList *list, void *data)
{
	Node *node = (Node*)malloc(sizeof(Node));
	node->data = data;
	if (list->head == NULL)
	{
		list->head = node;
	}
	else
	{
		list->tail->next = node;
	}
	list->tail = node;
	node->next = NULL;
}

int compareEmployee(Empolyee *e1, Empolyee *e2) 
{
	return strcmp(e1->name, e2->name);
}

typedef int(*COMPARE)(void*, void*);

// 使用函数指针可以传进不同的函数来执行比较操作
Node *getNode(LinkedList *list, COMPARE compare, void *data)
{
	Node *node = list->head;
	while (node != NULL)
	{
		if (compare(node->data, data) == 0)
		{
			return node;
		}
		node = node->next;
	}
	return NULL;
}

void delete(LinkedList *list, Node *node)
{
	if (node == list->head)
	{
		if (list->head->next == NULL)
		{
			list->head = NULL;
			list->tail = NULL;
		}
		else 
		{
			list->head = list->head->next;
		}
	}
	else
	{
		Node *tmp = list->head;
		while (tmp != NULL && tmp->next != node)
		{
			tmp = tmp->next;
		}
		if (tmp != NULL)
		{
			tmp->next = node->next;
		}
	}
	free(node); // 函数只释放节点,用户需要在调用之前删除节点指向的数据
}

void displayEmployee(Employee *employee)
{
	printf("%s\t%d\n", employee->name, employee->age);
	// '\t'是水平制表符,若前面有n个字符,将跳过8-n个空格
}

typedef void(*DISPLAY)(void*);

void displayLinkedList(LinkedList *list, DISPLAY display)
{
	Node *current = list->head;
	while (current != NULL)
	{
		display(current->data);
		current = current->next;
	}
}

// 使用方法
LinkedList linkedList;
initializeList(&linkedList);

Employee *samuel = (Employee*)malloc(sizeof(Employee));
strcpy(samuel->name, "Samuel"); // samuel->name是一个数组,所以不能直接赋值
samuel->age = 32;	

addHead(&linkedList, samuel);

Employee *susan = (Employee*)malloc(sizeof(Employee));
strcpy(susan->name, "Susan");
samuel->age = 25;

addTail(&linkedList, susan);

Node *node = getNode(&linkedList, compareEmployee, susan);
delete(&linkedList, node);

displayLinkedList(&linkedList, displayEmployee);
// Samuel  32

队列和栈可以基于链表实现

总结

认识指针、内存分配、数组、函数、字符串、结构体

你需要知道:

  • 指针、地址、解引怎么表示;
  • 如何动态分配内存并释放
  • 数组、字符串、结构体的内存是怎么分配的
  • 如何向函数传递(多维)数组和返回数组
  • 函数指针的用法和typedef声明(以sort函数和链表的实现为例)
  • 字符串字面量池与静态、动态分配字符串内存的不同
  • strcmp、strcpy、strcat的用法和注意事项
  • 函数返回指针、指针赋值(以stringToLower函数为例)
  • 结构体的内存与数组的不同、结构体内存的申请与释放
  • 通过指针实现链表及其中的各个功能函数
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值