征服C指针 摘录 第3章---解读C语言说明
一、混乱的声明——如何自然地理解 C 的声明?
通常,C 的声明
int hoge;
这样,使用“类型 变量名;”的形式进行书写。
可是,像“指向 int 的指针”类型的变量,却要像下面这样进行声明:
int *hoge_p;似乎这里声明了一个名为 *hoge_p 的变量,而实际上,这里声明的变量是 hoge_p,hoge_p 的类型是“指向 int 的指针”。
因为这种声明方式不太好理解,所以有人提出将 * 靠近类型这一侧进行书写,如下:
int* hoge_p;的确,这种书写方式符号“类型 变量名;”的形式。但是在同时声明多个变量的情况下就会出现破绽:
/* 声明 2 个“指向 int 的指针”? ——其实不是,此时 piyo_p 是 int 类型变量 */int* hoge_p, piyo_p;此外,数组也是 C 的一种类型,比如
int hoge[10];这样的写法,就不符合“类型 变量名;”的形式。
二、用英语来阅读
我认为像 int *hoge_p; 还有 int hoge[10]; 这样的声明方式很奇怪。
对于这种程序的声明方式,可能也有很多人感觉不到什么别扭的地方。那就再看下面的这个例子(经常被使用):
char *color_name[] = { "red", "green", "blue"};这里声明了一个“指向 char 的指针的数组”。
我们还可以像下面这样声明一个“指向将 double z作为参数并且返回 int 的函数的指针”:
int (*func_p)(double);关于这样的声明,在 K&R 中有下面这样一段说明:
int *f(); /* f: 返回指向 int 指针的函数 */和
int (*pf)(); /* pf: 指向返回 int 的函数的指针 */
这两个声明最能说明问题。在这里,因为 * 是前置运算符,它的优先级低于 (),为了让连接正确地进行,有必要加上括号。
首先,这段文字中有谎言。
声明中 *、() 和 [] 并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。
先将这个问题放在一边。如果你老老实实地去读这段文字,该会嘀咕“是不是搞反了”。如果说
int (*pf)();是指向函数的指针,使用括弧先将星号(指针)括起来岂不是很奇怪?
这个问题的答案,等你明白过来就会觉得非常简单。C 语言本来是美国人开发的,最好还是用英语来读。
以上的声明,如果从 pf 开始以英语的顺序来读,应该是下面这样:
pf is pointer to function returning int
翻译诚中文,则为
pf 为指向返回 int 的函数的指针。
三、解读 C 的声明
C 语言声明的方法:机械地向前读。
为了把问题变得更简单,我们在这里不考虑 const 和 volatile。接下来遵循以下步骤来解释 C 的声明。
- 首先着眼与标识符(变量名 或者 函数名)。
- 从距离标识符最近的地方开始,按照优先顺序解释派生类型(指针、数组 和 函数)。优先顺序说明如下,
- 用于整理声明内容的括弧
- 用于表示数组的[],用于表示函数的()
- 用于表示指针的 *
- 解释完成派生类型,使用 “of”,“to”,“returning”将它们连接起来。
- 最后,追加数据类型修饰符(在左边,int、double 等)。
- 英语不好的人,可以倒序用中文解释。
数组元素个数和函数的参数属于类型的一部分。应该将它们作为附属于类型的属性进行解释。
比如,
int (*func_p)(double);
(1) 首先着眼于标识符。
int (*func_p)(double)
英语的表达为:
func_p is
(2) 因为存在括号,这里着眼于 *。
int (*func_p)(double);
英语的表达为:
func_p is pointer to
(3) 解释用于函数的(),参数 double。
int (*func_p)(double);
英语的表达式为:
func_p is pointer to function(double) returning
(4) 最后,解释数据类型修饰符 int。
int (*func_p)(double);
英语的表达为:
func_p is pointer to function(double) returning int
(5) 翻译成中文:
func_p 是指向返回 int 的函数的指针。
使用和上面相同的方式,我们对各种各样的声明进行解读,如下表所示:
| C 语言 | 英语表达 | 中文表达 |
|---|---|---|
| int hoge; | hoge is int | hoge 是int |
| int hoge[10]; | hoge is array(元素个数10) of int | hoge 是 int 的数组(元素个数10) |
| int hoge[10][3]; | hoge is array(元素个数10) of array(元素个数3) of int | hoge 是 int 数组(元素个数3)的数组(元素个数10) |
| int *hoge[10]; | hoge is array(元素个数10) of pointer to int | hoge 是指向 int 的指针的数组(元素个数 10) |
| double (*hoge)[3]; | hoge is pointer to array(元素个数3) of double | hoge 是指向 double 的数组(元素个数3)的指针 |
| int func(int a); | func is function(参数为 int a) returning int | func 是返回 int 的函数(参数是 int a) |
| int (*func_p)(int a) | func_p is pointer to function(参数为 int a) returning int | func_p 是指向返回 int 的函数(参数为 int a)的指针 |
“使声明的形式 和 使用的形式相似”是 C (还有从 C 派生的 C++、Java 等语言)特有的 奇怪的语法。
四、类型名
在 C 中,除标识符以外,有时候还必须定义“类型”。具体来说,遇到以下情况需定义“类型”:
- 在强制转型运算符中
- 类型作为 sizeof 运算符的操作数
比如,将强制转型运算符写成下面的形式:
(int *)
这里指定“int *”为类型名。
从标识符的声明中,将标识符取出后,剩下的部分自然就是类型名。
如下表所示,类型名的写法:
| 声 明 | 声明的解释 | 类型名 | 类型名的解释 |
|---|---|---|---|
| int hoge; | hoge 是 int | int | int 类型 |
| int *hoge; | hoge 是指向 int 的指针 | int * | 指向 int 的指针类型 |
| double (*p)[3]; | p 是指向 double 的数组(元素个数 3)的指针 | double(*)[3] | 指向 double 的数组(元素个数 3)的指针类型 |
| void (*func)(); | func 是指向返回 void 函数的指针 | void (*)() | 指向返回 void 函数的指针类型 |
上表最后2个例子中,括起星号的括弧 (*) 好像有点多余,但是一旦去掉括弧,意思就完全不一样了。
1、(double *[3]) 是将 double *hoge[3] 的标识符去掉后形成的,所以这个类型名被解释成“指向 double 的指针的数组”。
2、int (*func_table[10])(int a) 可以解释成“指向返回 int 的函数(参数为 int a)的指针的数组(元素个数 10)”。
类型的链式表示
int (*func_table[10])(int a)
指向返回int 的函数 (参数为int a)的指针的数组(元素个数为1

从第2个元素开始的元素都是派生类型。最后的类型对这个类型的含义就有重要的意义。因此将他称为类型分类。
派生类型:
- 结构体
- 联合体
- 指针
- 数组
- 函数
以基本类型开头,通过递归的(重复的)增加派生类型,可以创造出无限多个类型。
指针类型可以由函数类型、对象类型、或不完全类型派生派生指针类型的类型称为被引用类型。指针类型描述了一种对象,其值只是用于被引用类型的实体,由被引用类型T派生的指针类型称为指向T的指针。由被引用类型构造指针类型的过程称为指针类型的派生。这些构造派生类型的方法,可以递归的应用。
和指针类型一样,数组类型也是从已有类型派生而来的。元素个数作为类型的属性信息添加在类型后面。
指向数组的指针。 int (*array_p)[3];
在表达式中数组的确会被解读成指针。但这里的指针是“指向数组初始元素的指针”,而不是指向数组的指针。
指向数组初始元素的指针与指向数组的指针二者是不同的,他们进行指针运算的结果也不相同
C语言不存在多维数组,看起来像是多为数组的,其实是“数组的数组”
例如 : int hoge[3][2]; 通过 hoge[i][j]访问
hoge[i]指的是Int的数组(元素个数为2)的数组(元素个数为3)中的第i个元素
因为在表达式中,数组会被解读为指针,所以作为参数传递时 函数原型 void func (int (*hoge)[2])
函数也是一种派生类型,以参数为属性。函数类型与其他的派生类型形式不同。除函数以外的类型,其实体基本上都可以定义为变量,而这些变量会占据一定的空间,我们可以通过sizeof运算符获取它们的长度,像这样可以取得长度的类型,在标准中被称为对象类型。函数类型不是对象类型,也无法确定其长度。从函数类型不能派生出指针类型以外的类型。从数组类型不能派生出函数类型。
类型的长度。
- 基本类型。基本类型的长度取决于运行环环境。
- 指针 指针的长度取决于运行环境,并且在大多数情况下是不受派生源的类型影响的固定长度。
- 数组 数组的长度可以通过派生源的类型的长度乘以数组元素的个数得到。
- 函数。函数类型的长度无法计算。
结构体是由其他多种类型整合而成。数组是由相同的元素排列而成,结构体整合的是不同的类型。结构体体是排列地分配各个成员的内存空间,联合体是重叠地分配。
不完全类型 是除了函数类型外的长度不确定的类型。
c语言类型可分为:对象类型(int、char、数组、指针、结构体等)、函数类型、不完全类型
基本表达式
- 标识符(变量名、函数名)
- 常量(整数常量、浮点常量、枚举常量、字符常量)
- 字符串字面量(用“”括起来的字符串)
- 用()括起来的表达式
对表达式使用运算符或者通过运算符将表达式与表达式连接起来的也是表达式
所有的表达式都有类型
sizeof使用方法
- sizeof (类型名)
- sizeof 表达式 返回表达式的类型长度
左值L-value中的L指的是Location,表示可寻址. The "l" in lvalue can be thought of as location.
右值R-value中的R指的是Read,表示可读. The "r" in rvalue can be thought of as "read" value.
数组在表达式中被转换为指针的例外:
当作为sizeof 运算符的操作数时 返回数组整体的长度
当作为&运算符的操作数时 数组前加& 返回的是指向数组整体的指针
初始化数组时的字符字面量 字符串字面量是"char的数组" 所以在表达式中被解读为 指向char的指针 但关于初始化char的数组时的字符串字面量,编译器会将其特别解释为花括号内字符分段书写的初始化列表的省略形式。
与数组和指针相关的运算符:
间接运算符
* 取指针作为操作数,返回其指向的对象或者函数.只要返回的不是函数,*的结果都是左值,返回的表达式类型就是从操作数类型中去掉一个指针之后的类型
&地址运算符 嗯。&取左值作为操作数,返回指向该左值的指针,其类型为操作数类型加上上一个指针之后的类型。地址运算符不能取非左值的表达式作为操作数
[]下标运算符 []取指针和整数作为操作数 p[i]是*(p + i)的语法糖
->运算符 结构体或联合体指针运算符 p->hoge是(*p).hoge 的语法糖
C语言的运算符众多,具有不同的优先级和结合性下边列表显示用于查询:
|
优先级 |
运算符 |
名称或含义 |
使用形式 |
结合方向 |
说明 |
|---|---|---|---|---|---|
|
1 |
[] |
数组下标 |
数组名[常量表达式] |
左到右 |
|
|
() |
圆括号 |
(表达式)/函数名(形参表) |
|||
|
. |
成员选择(对象) |
对象.成员名 |
|||
|
-> |
成员选择(指针) |
对象指针->成员名 |
|||
|
2 |
- |
负号运算符 |
-表达式 |
右到左 |
单目运算符 |
|
(类型) |
强制类型转换 |
(数据类型)表达式 |
|||
|
++ |
自增运算符 |
++变量名/变量名++ |
单目运算符 |
||
|
-- |
自减运算符 |
--变量名/变量名-- |
单目运算符 |
||
|
* |
取值运算符 |
*指针变量 |
单目运算符 |
||
|
& |
取地址运算符 |
&变量名 |
单目运算符 |
||
|
! |
逻辑非运算符 |
!表达式 |
单目运算符 |
||
|
~ |
按位取反运算符 |
~表达式 |
单目运算符 |
||
|
sizeof |
长度运算符 |
sizeof(表达式) |
|||
|
3 |
/ |
除 |
表达式/表达式 |
左到右 |
双目运算符 |
|
* |
乘 |
表达式*表达式 |
双目运算符 |
||
|
% |
余数(取模) |
整型表达式/整型表达式 |
双目运算符 |
||
|
4 |
+ |
加 |
表达式+表达式 |
左到右 |
双目运算符 |
|
- |
减 |
表达式-表达式 |
双目运算符 |
||
|
5 |
<< |
左移 |
变量<<表达式 |
左到右 |
双目运算符 |
|
>> |
右移 |
变量>>表达式 |
双目运算符 |
||
|
6 |
> |
大于 |
表达式>表达式 |
左到右 |
双目运算符 |
|
>= |
大于等于 |
表达式>=表达式 |
双目运算符 |
||
|
< |
小于 |
表达式<表达式 |
双目运算符 |
||
|
<= |
小于等于 |
表达式<=表达式 |
双目运算符 |
||
|
7 |
== |
等于 |
表达式==表达式 |
左到右 |
双目运算符 |
|
!= |
不等于 |
表达式!= 表达式 |
双目运算符 |
||
|
8 |
& |
按位与 |
表达式&表达式 |
左到右 |
双目运算符 |
|
9 |
^ |
按位异或 |
表达式^表达式 |
左到右 |
双目运算符 |
|
10 |
| |
按位或 |
表达式|表达式 |
左到右 |
双目运算符 |
|
11 |
&& |
逻辑与 |
表达式&&表达式 |
左到右 |
双目运算符 |
|
12 |
|| |
逻辑或 |
表达式||表达式 |
左到右 |
双目运算符 |
|
13 |
?: |
条件运算符 |
表达式1? 表达式2: 表达式3 |
右到左 |
三目运算符 |
|
14 |
= |
赋值运算符 |
变量=表达式 |
右到左 |
|
|
/= |
除后赋值 |
变量/=表达式 |
|||
|
*= |
乘后赋值 |
变量*=表达式 |
|||
|
%= |
取模后赋值 |
变量%=表达式 |
|||
|
+= |
加后赋值 |
变量+=表达式 |
|||
|
-= |
减后赋值 |
变量-=表达式 |
|||
|
<<= |
左移后赋值 |
变量<<=表达式 |
|||
|
>>= |
右移后赋值 |
变量>>=表达式 |
|||
|
&= |
按位与后赋值 |
变量&=表达式 |
|||
|
^= |
按位异或后赋值 |
变量^=表达式 |
|||
|
|= |
按位或后赋值 |
变量|=表达式 |
|||
|
15 |
, |
逗号运算符 |
表达式,表达式,… |
左到右 |
从左向右顺序运算 |
同一优先级的运算符,运算次序由结合方向所决定。
const
const为类型修饰符,用于修饰类型,表示只读
char const *src 与 const char *src 意思完全相同
char *strcpy (char *dest, const char *src)
char *my_strcpy (char *dest, const char *src)
{
src = NULL; //即使给src赋值,编译器也不会报错 因为此时变为只读的不是src 而是src指向的地址
}
char *my_strcpy (char *dest, const char *src)
{
*src = 'a'; //此时报错,不能更改src所指向的地址的内容
}
char *my_strcpy (char *dest,char *const src)
{
src = NULL; //报错,此时src 为只读
*src = 'a' //可以
}
char *my_strcpy (char *dest,const char *const src)
{
src = NULL; //报错,此时src与其地址 皆为只读
*src = 'a' //报错
}
char const * 不能赋值给char * 变量 也不能作为参数传递给char* 函数
typedef struct{ char *title; int price; char isban[32]; }BookData; void register_book(BookData const * book_data); //此时book_data为只读,可以通过book_data->title来修改title
typedef struct{
char const *title; //此时才不会被修改
int price;
char isban[32];
}BookData;
typedef为某个类型定义别名,可以按照与通常变了声明相同的方式解读typedef,只是他声明的是类型的别名,可以同时声明多个变量
void func(int size) { typedef int Array[size]; printf(“sizeof Array..%d\n",(int)sizeof(Array));//若int为4字节 则显示20 size = 10; printf(“sizeof Array..%d\n",(int)sizeof(Array));//修改size值后仍然显示20 }
只有在声明函数形参的情况下,int a[] 与 int *a 才具有相同的意义,对与多维数组(数组的数组),只有最外层的数组会被解读为指针
//void func( int a[5]) 和 void func(int *a)有相同的意义
//void func( int a[3][5]) 和 void func(int (*a)[3])有相同的意义
//void func( int a[][5]) 允许 void func(int a[][])和void func(int a[3][])不允许
因为他们都是表达的 返回指向数组的指针, 所以必须要知道这个返回的数组的参数
C99可实现 将纵横双向的多维度数组 传递给函数
void func( int size1, int size2, int a [size1][size2]);//接收size1×size2的二维数组
void func( int a [*][*], int size1, int size2);想要先声明数组,再声明数组长度的声明方式
二者都是声明方式
void func(int a[size1][size2], int size1, int size2);定义必须写成这样的形式
字符串字面量的类型是 char的数组 ,因此在表达式中他会被解读指向char 的指针
char *str; str = "abc"; char str[] = "abc"; 和char str[] = {'a','b','c','\0'};具有相同含义
//因为C语言原本只能处理标量的语言,所以之前是不能对自动变量的数组进行初始化的
//因此char str[] = "abc";不可以,需要写成 static char str[]=”abc“;才可以
//ANSI C 开始即使是自动变量的数组也可以一并初始化
//但仅限于初始化列表的情况
char str[4];
str = ”abc“;//这样不可以
char *str = ”abc“; 可以
char *color_name[]{ ”red“; ”green“; ”blue“ }//color_name为char 的数组(元素为6 )的数组 char color_name[][6]{ {‘r‘,’e',’d‘,’\0‘}, {’g‘,’r‘,’e‘,’e‘,’n‘,’\0‘}, {'b','l','u','e','\0'}, }//二者写法意思相同
char str[]= "abc"; str[0] = 'd'; //可以写 char * str = "abc"; str[0] = 'd'; //大多数运行时会报错
函数在表达式中也会被解读成为指向函数的指针
函数值表达式自动转换为指向函数的指针,但在作为地址运算符&或者sizeof运算符的操作数时例外
函数调用运算符()的操作数不是函数,而是指向函数的指针
*作用于指向函数的指针也不起作用
int func();func 为返回int 的函数 则 &func 得到 指向函数的指针
强制转换 (类型名)待转换变量
基本类型转换 值的内部表现发生变化
指针转换 对与编译器而言只是改变了类型信息,值的内部表现不发生变化.
不要用强制类型转换掩饰编译器爆出的警告
练习----解读复杂声明
在 ANSI C 的标准库中,有一个 atexit()函数。如果使用这个函数,当程序正常结束的时候,可以回调一个指定的函数。
atexit()的原型定义如下:
int atexit(void (*func)(void));
1、首先着眼于标识符。
int atexit(void (*func)(void));
英语的表达为:
atexit is
2、解释用于函数的()。
int atexit(void (*func)(void));
英语的表达为:
atexit is function() returning
3、函数的参数部分比较复杂,所以先解析这部分。同样地, 先着眼于标识符。
int atexit(void (*func)(void));
英语的表达为:
atexit is function(func is) returning
4、因为有括号, 所以这里解释*。
int atexit(void (*func)(void));
英语的表达为:
atexit is function(func is pointer to) returning
5、解释用于函数的()。这里的参数还是比较简单的, 是 void(无参数) 。
int atexit(void (*func)(void));
英语的表达为:
atexit is function(func is pointer to function (void) returning) returning
6、解释类型指定符 void。这样就结束了 atexit 的参数部分的解释。
int atexit(void (*func)(void));
英语的表达为:
atexit is function(func is pointer to function(void) returning void) returning
7、解释数据类型修饰符 int。
int atexit(void (*func)(void));
英语的表达为:
atexit is function (func is pointer to function (void) returning void) returning int
8、翻译成中文……
atexit 是返回 int 的函数(参数是,指向返回 void 没有参数的函数的指针) 。
下面是一个更加复杂的例子。
标准库中有一个 signal()函数,它的原型声明如下,
void (*signal(int sig, void (*func)(int)))(int);
1、首先着眼于标识符。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is
2、相比*, ()的优先顺序更高,所以先解释这部分。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function() returning
3、解释参数部分。这里有两个参数,第一参数是 int sig。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int,) returning
4、着眼另外一个参数。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is) returning
5、因为有括号, 所以这里解释*。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to) returning
6、解释表示函数的(), 参数为 int。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to function(int) returning) returning
7、解释数据类型修饰符 void。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to function(int) returning void) returning
8、参数部分已经解释结束。接着因为有括号,所以这里解释*。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to function(int) returning void) returning pointer to
9、解释表示函数的(),参数为 int。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to function(int) returning void) returning pointer to function(int) returning
10、最后,添上 void。
void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal is function(sig is int, func is pointer to function(int) returning void) returning pointer to function(int) returning void
11、翻译成中文……
signal 是返回“指向返回 void,参数为 int 的函数的指针”的函数,它有两个参数, 一个是 int,另一个是“指向返回 void 参数为 int 的函数的指针”。
如果能读懂这种难度的声明,我想应该不会再有什么让你畏惧的 C 声明了。
下面的说明可能会让你对 C 语言感到更加不快。
signal()是用于注册信号处理(当中断发生时被调用的函数)的函数。此函数的返回值是之前注册的处理当前信号中断的函数。
也就是说,其中的一个参数和返回值,它们都是相同的类型——指向信号处理函数的指针。在一般的语言中,同样的表现模式出现两次并不会让你感到不适,但是解释 C 语言声明的过程是“一会儿向左一会儿向右”,因此,表示返回值的部分散落了在左右两侧。
此时,运用 typedef 可以让声明变得格外得简洁。
/*摘录于FreeBSD 的man page /typedef void(sig_t)(int);sig_t signal(int sig, sig_t func);sig_t 代表“指向信号处理函数的指针”这个类型。
C语言数组与指针截然不同

浙公网安备 33010602011771号