|
彻底理解这种话说不出口,能越来越接近彻底理解即可
《征服C指针》笔记并不能涵盖书中的巧妙,还是希望大家可以自行阅读,把本文当作一个备忘录 blog链接:真的理解C语言么?–《征服C指针》笔记
前言
使用C语言开发了大半年了,对其中一些奇怪的约束和用法表示迷惑和不理解,《征服C指针》这本书是周末偶尔发现的,大致了解了一下,“这就是我要的”脱口而出,能让自己在热爱的计算机领域进步的书籍总是让人兴奋的,更何况是C语言相关的并且通俗易懂。
不将学到的知识记录下来甚是可惜,于是有了这篇文章。
打好基础
C语言是什么样的语言
C语言曾经是只能使用标量的语言。
什么是标量呢?:char、int、double、枚举类型等算术类型以及指针。相对的,像数组、结构体和联合体这种由多个标量组合而成的类型,我们称为聚合类型。
早期C语言能够一起实现的功能,只有将标量这种小巧的类型从右边放到左边(赋值),或者标量间的运算、标量间的比较等。
所以为什么不支持if (str == "abc")的原因浮出水面,字符串不是标量,他是char类型的数组,在C中不能用==一下子对数组里的所有元素进行比较。
关于指针
关于指针,K&R中有如下说明:
指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛 这里先介绍一下变量的概念,C程序里使用的变量值是被保存在内存中的。也就是说,各个变量都被分配了某个地址的内存。向变量赋值,就是把值保存在这个地址的内存中。在C语言中,单是保存整数的变量就有诸如char类型、short类型、int类型和long类型等多种类型。用来保存变量的内存上的空间叫做对象,而被保存为对象的数据类型叫做对象类型。
此外,C语言标准对于“指针”一词是如下定义的:
指针类型可以由函数类型、对象类型或不完全类型派生,派生指针类型的类型被称为引用类型 所以,可以这么概括:
- 指针类型是类型
- 指针类型由其他类型派生而来,例如其类型可以为指向int的指针类型
- 指针类型也是类型,也存在指针类型的变量,指针类型的值
- 先有指针类型,因为有了指针类型,所以有了指针类型的变量和指针类型的值
- 指针类型的值,实际上就是内存的地址
借着这个机会,纠正一下读法:
// 指向int的指针 类型 的 变量hoge_p
int *hoge_p;再看一段神奇的代码:
int *p = 3; // 警告
int *p = 0; // 无警告为什么第二行代码没有警告?在C语言中,在应当被当作指针处理的上下文中,0这个常量会被当作空指针处理。
关于数组
下标运算符[]与数组毫无关系! 选自书中原文,一开始就亮明这句话,体现它的重要性。
关键就在于,在表达式中,不论数组名后是否加[],数组都会被解读成指向其初始化元素指针,所以,有一句话是错的。
在C语言中,如果数组名后不加[],而只是写数组名,那么此名称就表示“指向数组初始元素的指针” 可以注意到,是表达式中,因为声明时用的*、&、[]与表达式中的他们是风马牛不相及的。
而且下标运算符也是一种运算符,他需要访问下标和指针,也算是一个二元运算符,既然二元运算符a + b可以改写为b + a,那么同理如下:
#include <stdio.h>
int main(void) {
int array[5];
int *p;
for (int i = 0; i < 5; i++) {
array = i;
}
p = &array[0];
for (int i = 0; i < 5; i++) {
printf(&#34;%d\n&#34;, *(p + i));
}
printf(&#34;=====================\n&#34;);
for (int i = 0; i < 5; i++) {
printf(&#34;%d\n&#34;, i[p]);
}
printf(&#34;=====================\n&#34;);
for (int i = 0; i < 5; i++) {
printf(&#34;%d\n&#34;, i[array]);
}
return 0;
}
// 结果是一样的所以,虽然有些违背常理,下标运算符[]的确与数组毫无关系,p只是*(p + i)的简便写法罢了,可以理解成一个语法糖。
在声明函数形参时,才可以将数组的声明解读为指针,也只有这种情况下int a[]和int *a具有相同的意义了:
// 一下形参声明是一个意思
int func(int *a);
int func(int a[]);
int func(int a[10]); // 编译器会直接无视元素个数有了以上知识背景,可以理解C语言为什么不进行数组边界检查了,这只是个语法糖啊,写的时候看上去制定了边界,但直接就被解读成指针了。
C语言是怎样使用内存的
小tips:fflush()是用于输出流的,不能用于输入流,在C语言标准中,fflush()用于输入流的行为时未定义的。
C语言中内存的使用方法
作用域:
存储器:
生命周期:
- 静态变量:生命周期从程序运行时开始,到程序关闭时结束
- 自动变量:生命周期直至程序离开该变量声明所带代码块为止
- 通过malloc()分配的内存空间:生命周期直至free()被调用为止
在C语言中,表达式的数组会被解读为指针,同样的,表达式中的函数也意味着指向函数的指针。
栈在运行时可以延伸,所以我们可以在栈上配置可变长数组(VLA),只有自动变量可以使用VLA。
函数与字符串字面量
在如今大多数操作系统中,函数主体与字符串字面量时一并配置在同一个只读内存区域中的。
书上写了写关于函数的汇编代码,眼见为实,自己调用看看,程序如下:
int add_func(int a, int b)
{
int result;
result = a + b;
return result;
}
int main()
{
int ans = add_func(1, 2);
return 0;
}汇编关键部分如下:

整体还是能猜出来的:
- 调用方将实参的值从后往前压入栈中
- 函数参数优先传递给了寄存器edi,esi,add_func中,将两个寄存器的值赋值给局部变量a,b
- a,b求和,最终放入eax,调用约定规定函数的返回值要保存在寄存器eax中
可变长参数
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap ); 说明:
1)va_list:一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
2)va_start:对ap进行初始化,让ap指向可变参数表里面的第一个参数。第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
3)va_arg: 获取参数。它的第一个参数是ap,第二个参数是要获取的参数的指定类型。按照指定类型获取当前参数,返回这个指定类型的值,然后把 ap 的位置指向变参表中下一个变量的位置;
4)va_end:释放指针,将输入的参数 ap 置为 NULL。通常va_start和va_end是成对出现。
利用malloc()动态分配内存
是否应该强制转换malloc()的返回值类型?
- ANSI C之前,C语言没有void *,所以malloc()的返回值为char *,需要强制转换
- ANSI C之后,malloc()返回值改为了void *,不需要转换
- C++中,无法将void *赋值给普通指针变量,需要强制转换
这小节关于内存的介绍以及下一小节对齐的介绍还是较为清晰的,要是不了解相关知识的可以去看看,暂时不做描述。
语法揭秘
解读C语言声明
C语言阅读起来有时候会反直觉,答案很简单:C语言原本是在美国诞生的语言,所以我们应该用英文来读,可以遵循以下规则:
- 先看标识符(变量名或函数名)
- 从贴近标识符的地方开始,按如下优先级解释派生类型(指针、数组、函数)
- 用于整合声明的括号
- 用于表示数组的[]、表示函数的()
- 表示指针的*
- 完成对派生类型的解释之后,通过of、to或returning连接句子
- 添加类型修饰符(位于左侧,比如int、double)
C语言 | 英语表达 | int hoge; | hoge is int | int hoge[10]; | hoge is array of int | int hoge[10][3]; | hoge is array of array of int | int *hoge[10]; | hoge is array of pointer to int | double (*hoge)[3]; | hoge is pointer to array of double | int func(int a); | func is funciton returning int | int (*func_p)(int a) ; | funcc_p is pointer to function returning int | 英文表达清晰多了,也不容易有歧义。
C语言数据类型的模型
指针:

数组:

数组和指针都是派生类型,单独都较为清晰,混在一起呢,比如指向数组的指针。
一听到“指向数组的指针”,有人也许要说: 这不是很简单嘛,数组名后不加[],不就是“指向数组的指针”吗? 抱有这个想法的人,请将 前文重新阅读一下!
的确,在表达式中,数组可以被解读成指针。但是,这不是“指向数组的指针”,而是“指向数组初始元素的指针”。
int main()
{
int (*array_p)[3];
int array[3];
array_p = &array;
// array_p = array;
// warning: assignment to &#39;int (*)[3]&#39; from incompatible pointer type &#39;int *&#39; [-Wincompatible-pointer-types]
return 0;
}但注意到,这只是warning,没有强制报错,从地址的角度来看,array 和&array也许就是指向同一地址。但要说起它们的不同之处,那就是它们在做指针运算时结果不同。
因为int 类型的长度是4个字节,所以给“指向 int 的 指针”加1,指针前进4个字节。
但对于“指向 int 的数组(元素个数 3)的指针”,这个指针指向的类型为“int 的数组(元素个数 3)”,当前数组的尺寸为12个字节(如果 int 的长度为 4 个字节),因此给这个指针加1,指针就前进12 个字节。

C语言不存在多维数组
int hoge[3][2]的读法是什么:hoge ia array of array,是数组的数组,有多维数组么,多维是便于逻辑上理解的概念,实际排布如下:

如果函数的形参需要是所谓的“多维数组”,该怎么声明函数呢?如下都是可以的,要记住,函数会被解读成指针,上文提到:在声明函数形参时,才可以将数组的声明解读为指针,也只有这种情况下int a[]和int *a具有相同的意义了。
void func(int (*hoge)[2]);
void func(int hoge[3][2]);
void func(int hoge[][2]);函数类型的派生
函数类型也是一种派生类型,“参数(类型)”是它的属性。

下文原封不动来自书本,特地使用引用,最好理解这段话。
可是,函数类型和其他派生类型有不太相同的一面。
无论是int还是double,亦或数组、指针、结构体,只要是函数以外的类型,大体都可以作为变量被定义。而且,这些变量在内存占用一定的空间。
因此,通过sizeof运算符可以取得它们的大小。 像这样,有特定长度的类型,在标准中称为对象类型。
可是,函数类型不是对象类型。因为函数没有特定长度。 所以C中不存在“函数类型的变量”(其实也没有必要存在)。
数组类型就是将几个派生类型排列而成的类型。因此,数组类型的全体长度为: 派生源的类型的大小×数组的元素个数。
可是,函数类型是无法得到特定长度的,所以从函数类型派生出数组类型是不可能的。也就是说,不可能出现“函数的数组”这样的类型。
可以有“指向函数的指针”类型,但不幸的是,对指向函数类型的指针不能做指针运算,因为我们无法得到当前指针类型的大小。
此外,函数类型也不能成为结构体和共用体的成员。
总而言之:从函数类型是不能派生出除了指针类型之外的其他任何类型的。
不过“指向函数的指针类型”,可以组合成指针或者作为结构体、共用体的的数据类型的成员。
毕竟“指向函数的指针类型”也是指针类型,而指针类型又是对象类型。 另外,函数类型也不可以从数组类型派生。 表达式
基本表达式:
- 标识符(变量名、函数名)
- 常量(包括整数常量和浮点数常量)
- 字符串常量(使用“”括起来的字符串)
- 使用()括起来的表示式
表达式代表某处的内存区域的时候,我们称当前的表达式为左值(lvalue);相对的是,表达式只是代表值的时候,我们称当前的表达式为右值。
在表达式中,数组会被解读为指针,除了以下三个情况:
- 当作为sizeof操作数时,返回的是数组整体的长度
- 作为&运算符操作数时,这个上文有例子,指向数组的指针
- 初始化数组时的字符串字面量。我们都知道字符串常量是“char 的数组”,在表达式中它通常被解读成“指向char 的指针”。其实,初始化char的数组时的字符串常量,作为在花括号 中将字符用逗号分开的初始化表达式的省略形式
数组被解读为指针时,该指针不是左值,所以以下代码是错误的:
char str[10];
str = &#34;abc&#34;;只能对左值赋值。
解读C语言声明(续)
const 是在ANSI C中追加的修饰符,它将类型修饰为“只读”。
char *my_strcpy(char *dest, const char *src)
{
src = NULL; // ←即使对 src 赋值,编译器也没有报错
}
// 此时,成为只读的不是 src,而是 src 所指向的对象。
char *my_strcpy(char *dest, const char *src)
{
*src = &#39;a&#39;; // ←ERROR!!
}
// 如果将 src 自身定义为只读,需要写成下面这样:
char *my_strcpy(char *dest, char * const src)
{
src = NULL; // ←ERROR!!
}
// 如果将 src 和 src 指向的对象都定义为只读,可以写成下面这样:
char *my_strcpy(char *dest, const char * const src)
{
src = NULL; // ←ERROR!!
*src = &#39;a&#39;; // ←ERROR!!
} 字符串字面量
用&#34;&#34;包裹的字符串称为字符串字面量,类型时char的数组,保存在只读区域,在表达式中,会被解读为指向char的指针。
可是,char 数组的初始化是个例外。此时的字符串常量,作为在花括号中分开书写的初始化表达式的省略形式,编译器会进行特殊处理。
char str[] = &#34;abc&#34;;
char str[] = {&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;\0&#39;};从ANSI C开始,即使是自动变量的数组,也可以被整合来进行初始化。
关于指向函数的指针引发的混论
对于 C 语言,表达式中的函数可以被解读成 “指向函数的指针”。
/*如果发生 SIGSEGV(Segmentation falut),回调函数 segv_handler */
signal(SIGSEGV, segv_handler);
signal(SIGSEGV, &segv_handler); // 尽然也是对的
func_p();
(*func_p)(); // 也是对的上述代码看了之后很迷茫,都解读成指针了,怎么是否取地址,解引用都一样呢?
为了照顾到这种混乱,ANSI C 标准对语法做了以下例外的规定:
- 表达式中的函数自动转换成“指向函数的指针”。但是,当函数是地址运算符&或者sizeof运算符的操作数时,表达式中的函数不能变换成 “指向函数的指针”
- 函数调用运算符()的操作数不是“函数”,而是“函数的指针”
如果对“指向函数的指针”使用解引用*,它暂时会成为函数,但是因为在表达式中,所以它会被瞬间地变回成“指向函数的指针”。
结论就是,即使对“指向函数的指针”使用*运算符,也是对牛弹琴,因 为此时的运算符*发挥不了任何作用。
阅读完书上这段话的时候我就觉的一言难尽……毕竟C语言历史较久,包容一下。
解读复杂声明
int atexit(void (*func)(void));
/*
* atexit is function
* para is func
* returning int
* func is pointer to function returning void
*/
void (*signal(int sig, void (*func)(int)))(int);
/*
* signal function, para is sig and func
* sig is int
* func is pointer to function, para is int returning void
* signal function returning pointer to function which para is int returning void
*/我的感觉是就别翻译成中文了,直接用英文梳理比较方便。
请记住:数组与指针截然不同
大家都说C语言的指针比较难,可是真正地让初学者“挠墙”的,并不是指针自身的使用,而是“混淆了数组和指针”。此外,很多“坑爹”的入门书对指针和数组的讲解也是极其混乱。 数组和指针的常见用法
这一章节主要是代码上的例子,翻阅即可。
遇到了一个还是容易有歧义的例子。
double (*p[5])[2];因为[]的优先级比*高,所以一上来就是:p is an array!
读法如下:
- p is an array
- each element is a pointer to array of double
double (*p[5])[2];
double element[2] = {1.0, 2.0};
p[0] = &element;
printf(&#34;%lf\n&#34;, (*p[0])[1]);数据结构
第五章主要是指针在数据结构上的应用。
拾遗
指定初始化
typedef struct {
int a;
double b;
int array[10];
char *str;
char array2[4];
} Hoge;
void printfHoge(Hoge hoge)
{
printf(&#34;hoge a:%d\n&#34;, hoge.a);
printf(&#34;hoge b:%lf\n&#34;, hoge.b);
printf(&#34;hoge array[4]:%d\n&#34;, hoge.array[4]);
printf(&#34;hoge array[5]:%d\n&#34;, hoge.array[5]);
printf(&#34;hoge str:%s\n&#34;, hoge.str);
printf(&#34;hoge array2:%s\n&#34;, hoge.array2);
return;
}
int main()
{
Hoge hoge = {
.a = 1,
.b = 2.0,
.array = {[4] = 5, 3},
.str = &#34;hello&#34;,
.array2 = {&#39;b&#39;, &#39;y&#39;, &#39;e&#39;, &#39;\0&#39;}
};
printfHoge(hoge);
Hoge hoge2 = (Hoge){
.a = 1,
.b = 2.0,
.array = {[4] = 5, 3},
.str = &#34;hello&#34;,
.array2 = {&#39;b&#39;, &#39;y&#39;, &#39;e&#39;, &#39;\0&#39;}
};
printfHoge(hoge2);
return 0;
}直接对结构体的元素指定值并初始化,或者传递一个结构体字面量都是可以的。 |
|