3.1 解读C的声明

3.1.1 用英语来阅读

char *color_name[]={
"red",
"green",
"blue",
};



声明一个“指向char的指针的数组”。

int *f();//返回指向int指针的函数
int (*pf)();//指向返回int的函数的指针




此处,因为*是前置运算符,它的优先级低于(),为了让连接正确地进行,有必要加上括号。


声明中* 、()、[]并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。

 

C语言是美国人开发的,最好还是用英语来读。以上的声明,如果pf开始以英语的顺序来读,应该如下:

pfis pointer to function returning int

翻译成中文,pf为指向返回int的函数的指针。

要点:用英语来读C的声明。




3.1.2解读C的声明

-首先着眼于标识符。

-从距离标识符最近的地方开始,依照优先顺序解释派生类型(指针、数组和函数)。优先顺序说明如下,

1)用于整理声明内容的括弧。2)用于表示数组的[],用于表示函数的()。3)用于表示指针的*。

-解释完成派生类型,使用of、to、returning将它们连接起来。

-最后,追加数据类型修饰符。

 

Eg:int (*func_p) (double);

标识符是(*func_p),英语表达是func_p is。

存在括弧,这里着眼于*,英语表达是 func_pis pointer to。

解释用于函数的(),参数是double。英语表达是 func_pis pointer to function (double) returning。

最后,解释数据类型修饰符int,英语表达为 func_pis pointer to function (double) returning int。

func_p是指向返回int的函数的指针。



解读各种各样C语言声明

int hoge;

hoge is int

hoge是int

int
hoge[10]

hoge is array of int

hoge是int的数组

int hoge[10][3]

hoge is array of array of int

hoge是int数组的数组

int *hoge[10]

hoge is array of pointer to int

hoge是指向int的指针的数组

int (*hoge)[3]

hoge is pointer to array of int

hoge是指向int数组的指针

int func(int a)

func is function returning int

func是返回int的函数

int(*func) (int a)

func is pointer to function returning int

func是指向返回int的函数的指针


3.1.3类型名

以下情况需定义“类型”:

-在强制转型运算符中。
-类型作为sizeof运算符的操作数。

类型名写法

声明

声明解释

类型名

类型名解释

int hoge;

hoge是int

int

int类型

int *hoge

hoge是指向int的指针

int *

指向int的指针类型

double (*p) [3]

p是指向double的数组的指针

double(*) [3]

指向double的数组的指针类型

void(*func)();

func是指向返回void函数的指针

void(*)()

指向返回void函数的指针类型




3.2 C的数据类型的模型

3.2.1 基本类型和派生类型

int(*func_table[10])(int a );//指向返回int的函数(参数为int a)的指针的数组(元素个数为10)

 

派生类型:结构体、共用体、指针、数组、函数。

从基本类型开始,递归地重复地粘附上派生类型,就可以生成无限类型。

 3.2.2 指针类型派生

指针类型:可由函数类型、对象类型、或不完全的类型派生,派生指针类型的类型称为引用类型。指针类型描述一个对象,该类对象的值提供对该引用类型实体的引用。由引用类型T派生的指针类型有时称为“指向T的指针”。从引用类型构造指针类型的过程称为“指针类型的派生”。这些构造派生类型的方法可以递归应用。

对于指针类型来说,因为它指向的类型各不相同,所以都是从既存的类型派生出“指向T的指针”这样的类型、

如果对指针进行加法运算,指针只前进指针所指向类型的大小的距离。

 3.2.3 数组类型派生

数组类型本质就是将一定个数的派生源的类型进行排列而得到的类型。

 3.2.4 什么是指向数组的指针

在表达式中,数组可以被解读为指针。但,这不是“指向数组的指针”,而是“指向数组初始元素的指针”。

 

int(*array_p)[3];//array_p是指向int数组(元素个数3)的指针。

 

在数组前加上&,可以获取执行数组的指针。

intarray[3];

int(*array_p)[3];

array_p=&array[3];

 3.2.5 C语言中不存在多维数组

C中存在数组的数组,但是不存在多维数组。

数组就是将一定个数的类型进行排列而得到的类型。“数组的数组”只不过是派生源的类型恰好为数组。

3.2.6 函数类型派生

函数类型也是一种派生类型,“参数类型”是它的属性。

有特定长度的类型,在标准中称为对象类型。

函数类型不是对象类型。因为函数没有特定长度。所以C中不存在“函数类型的变量”

 

数组类型就是将几个派生类型排列而成的类型。因此数组类型的全体长度为:派生源的类型大小* 数组元素个数

函数类型是无法得到特定长度的,所以从函数类型派生出数组类型是不可能的。也就是说,不可能出现”函数的数组“这样的类型。

 

要点:从函数类型是不能派生出除了指针类型之外的其他任何类型的。

从数组类型是不能派生出函数类型的。

 3.2.7 计算类型的大小

注意:c语言标准并没有对int、double和指针的大小进行任何规定,数据类型的大小完全取决于各处理环境的具体实现。

通常不需要去留意数据类型的物理大小,更不应该依赖数据类型大小进行编程。

 3.2.8 基本类型

派生类的底层是基本类型。

基本类型是指:char、int、float、double、枚举类型。统称为算术型。

 3.2.9 结构体和共用体

-虽然结构体和共用体在语法上属于派生类型,但是在声明中它和数据类型修饰符处于相同的位置。

-只有派生指针、数组和函数的时候,类型才可以通过一维链表表示。结构体、共用体派生类型只能用树结构进行表现。

 

结构体类型可以集合几个其他不同的类型,而数组只能线性地包含同一个类型。

 3.2.10 不完全类型

C的类型分为:

-对象类型(char、int、数组、指针、结构体)

-函数类型

-不完全类型

 

C标准中,void类型也被归类为不完全类型。




3.3 表达式

3.3.1 表达式和数据类型

基本表达式是指:

-标识符(变量名、函数名)

-常量

-字符串常量

-使用()括起来的表达式

 

所有表达式都持有类型。

 3.3.2 左值

作为变量。它有作为”自身的值“使用和作为”自身的内存区域“使用两种情况。

在C中,即使不是变量名,表达式也可以代表”某个变量的内存区域“。

表达式代表某处的内存区域的时候,称当前的表达式为左值。表达式只是代表值的时候,成当前表达式为右值。

 

3.3.3 将数组解读成指针

inthoge[10];

以上声明中,hoge等同于&hoge[0];

 

数组被解读成指针的时候,该指针不能作为左值。

-数组为sizeof运算符的操作数。(此时数组被当做指针,得到的几个是指针自身的长度)

-数组为&运算符的操作数。(通过对数组使用&,可以返回指向整体数组的指针)

-初始化数组时的字符串常量。(字符串常量是char的数组,在表达式中通常被解读成指向char的指针。初始化char的数组时的字符串常量,作为在花括号中将字符用逗号分开

的初始化表达式的省略形式,会被编译器特别解释。)

 

3.3.4 数组和指针相关的运算符

*解引用,运算符*将指针作为操作数,返回指针所指向的对象或者函数。只要不是返回函数,运算符*的结果都是左值。

&地址运算符,&将一个左值作为操作数,返回指向该左值的指针。对左值的类型加上一个指针就是&运算符的返回类型。

[]下标运算符,将指针和整数作为操作数。p[i]是*(p+i)的语法糖,此外没有任何意义。

->运算符,p->hoge是(*p).hoge;的语法糖、

 

3.3.5 多维数组

C语言不存在多维数组、运算符优先级


第3章 揭秘C的语法_数组



注意:优先级最高的是(),进行单目运算符的+、-、和*的优先级高于进行双目运算的+、-、*。

*p++:根据BNF规则,后置的++比前置++和*等运算符的优先级高,()和[]优先级相同。而K&R中的解释是,尽管*和++优先级相同,但是连接规则是从右往左,所以p和++先进行连接。


3.4 解读C的声明

3.4.1 const修饰符

const是在ASIC C中追加的修饰符,它将类型修饰为”只读“。


const 不一定代表常量。 const 主要被用于修饰函数的参数。将一个常量传递给函数没有意义的。使用 const 修饰符,只意味着使其 ” 只读 “ 。



char *strcpy(char *dest,const char *src);

strcpy是持有被const修饰的参数的范例。

char *my_strcpy(char *dest,const char *src){  
	    src=NULL;//即使对src赋值,编译器也没有报错   
}  

char *my_strcpy(char *dest,const char *src){  
	    *str='a';//error   
}   
char *my_strcpy(char *dest,char * const src){  
	    src=NULL; //ERROR  
}  
char *my_strcpy(char *dest,const char * const src){  
	src=NULL;//error  
    *src='a';//error  
}




在现实中,当指针作为参数时,const常用于将指针指向的对象设定为只读。

 

在原型声明中加入了const,尽管函数接受了作为参数的指针,但是指针指向的对象不会被修改。也就是说,函数虽然接受了指针,但是并不意外着要向调用方返回值。

const修饰的紧跟在它后面的单词。

 

3.4.2 如何使用const?可以使用到什么程度?

在一些通用函数中,如果本应该是const的参数却没有加上const,会经常导致调用方无法使用const。

Java中的String也是不可变的,这种方式有它的益处,但是有时候也会带来麻烦。


3.4.3 typedef

用于给某类型定义别名、
比如 typedefchar *String;//通过以上的声明,以后对于”指向char的指针“可以使用”String“这个别名。

 

要点:typedef使用和通常的标识符声明相同的方式进行解释。

可是,被声明的不是变量或者函数,而是类型的别名。

 

3.5 其他

3.5.1 函数的形参声明

在C中是不能将数组作为函数的参数进行传递的。无论如何,在这种情况下,只能传递指向数组初始元素的指针。

在声明函数形参时,作为类型分类的数组,可以被解读成指针。

void func(int a[]) {  
    ...  
}

可以被自动地解读成

void func(int *a) {  
	    ...  
}



此时,就算定义了数组的元素个数,也会被无视。

 

要点:只有在声明函数形参的情况下,inta[]和int *a才具有相同的意义。

void func(int a[][5])



a的类型是int的数组(元素个数5)的数组(元素个数不明),因此它可以解读成”指向int数组(元素个数5)的指针“。上面的声明本意是

void func(int(*a)[5])



3.5.2 关于空的下标运算符[]

C语言中,遇到以下情况下标运算符[]可以将元素个数省略不写。

对于这些情况,不同编译器会有各自特别的解释,不能作为普遍的规则来使用。

1)函数形参的声明。

2)根据初始化表达式可以取得数组大小的情况。

3)使用extern声明全局变量的情况(全局变量在多个编译单元中的某一个中定义,然后从其他代码文件通过extern进行声明。)

 

补充:定义和声明,在C中,”声明“在规定变量或者函数的实体的时候被称为”定义“。函数的原型是声明,函数的定义是指写着函数的实际执行代码的部分。

 

3.5.3 字符串常量

字符串常量的类型是”char的数组”,因此在表达式中,可以解读为指针。

char*str;

str="abc";//将[指向abc的初始元素的指针]赋给str。

 

通常,字符串常量保存在只读的内存区域。(准确来说,实际的保存方式还是要依赖处理环境的具体实现)。但如果在初始化char的数组的时候,采取将原本在花括号中分开书写的初始化表达式的省略形式,并且不给数组自身指定const,字符串常量就是可写的。

char str[]="abc";  
str[0]='d';//可写   
char *str="abc";  
str[0]='d';//大部分处理环境中会报错



补充:字符串常量是char数组。但是在表达式中可以被解读成“指向char的指针”

 

3.5.4 关于指向函数的指针引起的混乱

-表达式中的函数自动转换成“指向函数的指针”。但是,当函数是地址运算符&或者sizeof运算符的操作数时,表达式中的函数不能变换成“指向函数的指针”。

-函数调用运算符()的操作数不是“函数”,而是“函数的指针”。

 

如果对“指向函数的指针”使用解引用*,它暂时会成为函数,但是因为在表达式中,所以它会被瞬间变回成“指向函数的指针”。

结论:即使对指向函数的指针使用*运算符,也是对牛弹琴。因为此时的运算符*发挥不了任何作用。


3.5.5 强制类型转换

cast将某类型强制转换成其他类型的运算符,写成下面这样:

(类型名称)

两种强制转换:基本类型的强制转换、指针类型强制转换。



double double_var;  
int *int_p;  	  
int_p=(int *)&double_var;//将指向double的指针转换成指向int的指针


一旦将指向double指针强制转换为指向int的指针,就无法追踪指针原本指向什么对象。

编译器是不会无端给出警告的,强制类型转换只是暂时掩盖了问题。就算通过编译,程序也很有可能不会正常运行,要不就是虽然在当前环境中正常运行,一拿到别的环境中就跑步起来。

要点:不要使用强制类型转换来掩盖编译器的警告。

3.6 数组和指针是不同的事物

3.6.1 为什么引起混乱

C语言的数组和指针是完全不同的。

数组是一些对象排列后形成的,指针则表示指向某处。

 

int*p;

p[3]=....//突然使用没有指向内存区域的指针

-自动变量的指针在初期状态,值是不定的。

 

charstr[10];

str="abc";//突然向数组赋值

-数组既不是标量,也不是结构体,不能临时使用。

 

intp[];//使用空的[]声明局部变量

-只有在”函数的形参的声明中“,数组的声明才可以被解读成指针。

 

3.6.2 表达式之中

对于int *p;如果p指向了某个数组,自然可以通过p[i]的方式进行访问,但这并不代表p就是指针。


3.6.3 声明

只有在声明函数的形参时候,数组的声明才能解读成指针的声明。