C专家编程 笔记

C语言中的符号重载

C语言非常的简洁, 以至于不愿意用太多的符号, 这样有很多符号在不同的地方有不同的含义

这样会让用户很困惑, 这是c的语言特性, 也是设计上的一些失误

static    
在函数内部,表示该变量的值在各个调用间一直保持延续性;
对于函数,表示该函数只在本文件中可见

extern
 
用于变量,表示该变量在其它地方定义;
用于函数定义, 表示全局可见(属于冗余的)

void     
用于参数列表中,表示该函数参数为空,如int main(void);
用于返回值,表示该函数返回void,即不返回任何值,相当与pascal中的过程;
在指针声明中,表示通用指针


乘法运算符; 
用于指针前,表示对指针所指内容的间接引用;
用于声明中表示指针,如int *pi,表示指向整形的指针,char *strcpy(...)表示函数的返回值为指针

&      
位与运算符;
取地址操作符

() 
函数定义中,包围函数形式参数表,如int main(int argc, char **argv);
调用一个函数,如srand();
改变表达式的运算顺序,如 a * ( b + c );
类型转换, 如 (int*)x;
定义带参数的宏,如 #define MAX(a, b) ((a) > (b)) ? (a) : (b)
包围sizeof操作符的操作数(如果是类型名),如 sizeof(int)

 

数组和指针的本质区别

C编程新手最常听到的一句话是'数组和指针是相同的', 不幸的是, 这是一种危险的说法, 他并不正确.

最典型的一个错误是

file 1:
int mango[100];
file 2:
extern int *mango;

这样的声明是错的, 必须声明为extern int  mango[];

关于声明的意义在上篇讲解extern时已经详细说明过

 

我们首先要区分"地址y"和"地址y的内容"之间的区别, 这是一个相对微妙之处, 因为大多数编程语言中我们用同一个符号来表示两样东西, 并由编译器根据上下文环境来判断它的具体含义.

int X, Y;

X = Y;

这边我们就来深入讨论一下这个赋值语句, 看上去X和Y是同一种东西, 都是int型变量

但对于编译器而言, 一个赋值语句两边的东西是不一样的 (以汇编的角度思考)

左值  =  右值;

左值是地址, 右值是数据, 是有本质区别的

所以上面的X是X代表的地址, Y是Y代表的地址里面的内容

所以对于编译器而言,

X=2, 这个很直接, 把2放到X代表的地址上

X=Y, 需要先从Y代表的地址上读出数据, 再放到X代表的地址上

需要注意, 左值是在编译时就可以明确知道的, 因为编译器会为每个变量分配一个地址

而右值往往只有在运行时才可知的

 

那么下面详细看看为什么大家会误认为两者相同

int a[10], *p, *q;
p = a ;
q = p+1 ;

q = a+1;

int i;

i = a[0];

i = (*a);

i = (*p);

如上的代码, 你发现可以象操作指针一样来操作数组变量名a, 貌似a和指针p时等价的

其实非也, a其实代表数组的首地址, 就是个常量, 进行q = a+1, 类似于X = 2+1

而p是指针变量, p=a后, p代表的地址里存放了a,即数组首地址

那么q = p+1, 需要从p代表的地址中读出a, 并放到q代表的地址中去, 类似于X = Y+1

同样对于*操作, 是取地址上的内容, *a直接取a地址上的内容, 而*p是要先从p代表的地址取出地址数据, 再取该地址数据上的内容

而且两者还有一个区别是时间上的差别

对于q = a+1, 因为a代表常量, 在编译时就可以算出q的值

而对于q = p+1, p是变量, 必须到运行时才能知道q的值

 

两者是有本质区别的, a代表常量, 有人把它称为'常量指针', 是不能赋值或改变的指针, 这绝对的是误导

a就不是指针, 根本就不能作为左值, 你有见把2作为左值的吗

此处可以认为a等同于&p

之所以大家会混淆, 完全是因为c语言的设计不规范导致的, 相同的语句却有着完全不同的操作

 

此处个人觉得是数组的设计欠缺妥当

int i, * p;

对于一般的变量, 无论是i还是p, 作为左值表示变量名所代表的地址, 作为右值表示变量名所代表的地址上存放的数据. 其实i和p从这个层面上来讲, 完全没有什么分别, 只不过p中存放的数据是地址而不是其他.

 

但是对于int a[];

就不一样了, 因为a只是作为一个数据集合的开始位置, a无法作为左值(设计者认为这样的不恰当). 同时a作为右值时, 也无法取a地址上的数据, 因为它只是个开始位置, 你不知道取多少. 所以设计者打破了普通变量的规则, 把a当作一个地址常量来看待, 即当a作为右值时, 直接取a代表的地址作为右值, 而不会象通常变量一样去取地址上的数据作为右值.

问题是, 根据c的语法, 其在使用时和指针又十分相似, 这样的设计是不严谨的. 其实如果这样设计会稳妥些,

将a[]作为一个整体, 不能单独对a做任何操作, 如需取地址则用&(a[])

不过这样应该不符合c语言的简单美学...c语言是灵活, 简单的语言, 但是很多设计太随意, 不够严谨, 所以导致很多地方不符合逻辑, 很难理解

 

再论数组

本书和很多讲解数组和指针的文章过于复杂, 我个人觉得那是把简单的问题复杂化了.

我在上面应该已经把数组和指针的关系讲清楚了, 下面补充几点

1. 书中指出数组和指针在作为函数参数时是一样的(equivalent), 这个说法来自"C Programming Language, 2nd Ed, Kernighan & Ritchie"

如下,

char my_array[10];
char * my_ptr;
...
i = strlen( my_array );
j = strlen( my_ptr );

在c语言中, 作为函数的参数都是传值的, 但是数组却是个例外, 确实对于大数组, 传值明显是相当低效的

所以当你自己把数组作为函数参数的时候, 编译器其实是生成一个指向数组首地址的指针, 并把这个指针作为参数传入函数.

其实如果想把整个数组传值传入函数, 象前面说的, 只要把数组封装在结构中, 即可, 不过不推荐这样, 甚至在把结构体对象当作参数的时候, 要check结构体内是否有数组, 如果有数组, 最好把地址作为参数.

所以在作为函数参数时, 数组首地址会被转化为指针作为参数, 这个只是编译器出于方便统一这样处理.

在这点上说两者equivalent, 我个人觉得是不妥当的

 

2. 书中说"表达式中的数组名就是指针", 这个说法是相当危险的, 会把刚有些明白的程序员再次绕晕

int a[100];

int *p;

p = a+1;

书中的意思是, 这个操作中, 编译器出于处理的方便, 会先把a封装成一个指针, 然后赋值给p, 所以可以把a看作是指针, 这个说法很不负责

编译器可以出于处理方便的需要把a封装成指针, 但是数组名a绝对不是指针, 如果a是指针, 就应该可以作为左值进行赋值, a = p

实际上是不行的, a就是地址常量

 

3. 数组中的[]符号就代表地址偏移

在编译的时候, a[i] 会被改写成 *(a+i)

对于+是没有前后顺序的

所以你这样写, i[a], 也是可以的, 因为在编译时, 也是被改写成 *(i+a)

所以虽然看着很bt, 但是其实效果是一样的, 这就是[]的真正意义.

本文章摘自博客园,原文发布日期:2011-07-05

时间: 2024-10-27 20:48:14

C专家编程 笔记的相关文章

《C专家编程》一导读

前 言 C专家编程 C代码.C代码运行.运行码运行-请! --Barbara Ling 所有的C程序都做同一件事,观察一个字符,然后啥也不干. --Peter Weinberger 你是否注意到市面上存有大量的C语言编程书籍,它们的书名具有一定的启示性,如:C Traps and Pitfalls(本书中文版<C陷阱与缺陷>已由人民邮电出版社出版), The C Puzzle Book, Obfuscated C and Other Mysteries,而其他的编程语言好像没有这类书.这里有一

Android高级编程笔记(四)深入探讨Activity(转)

在应用程序中至少包含一个用来处理应用程序的主UI功能的主界面屏幕.这个主界面一般由多个Fragment组成,并由一组次要Activity支持.要在屏幕之间切换,就必须要启动一个新的Activity.一般的Activity都占据了整个显示屏,但可以创建成半透明或二者浮动的Activity. 一.创建Activity 通过继承Activity类可以创建一个Activity窗口,基本框架如下: 1 public class MyActivity extends Activity { 2 @Overri

JavaScript 模块化编程(笔记)

 一直对JS都是一知半解,最近遇到这方面问题,所以在网上学习了一下,现在还没有完全明白,先贴出笔记; 第一章 JavaScript模块化编程 (一):模块的写法 一 原始写法 // 模块就是实现特定功能的一组方法;只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块; function m1(){ // ... } function m2(){ // ... } // 上面的函数m1()和m2(),组成一个模块;使用时直接调用就行; // 缺点:"污染"了全局变量; 无

《JavaScript专家编程》——9.4 度量JavaScript代码质量

9.4 度量JavaScript代码质量 为了让计算精度上升到最高,客观质量分析以程序化的方式对代码进行分析.这项任务可以使用编程工具完成,这些工具能够在多种情况下评估代码,根据各项指标得到最终的质量得分.本节介绍了静态代码分析,这种方法非常适合评估JavaScript的质量. 静态代码分析 静态代码分析就是不通过运行代码来分析代码的过程.静态分析看起来非常像一个文本编辑器的拼写检查器.拼写检查器扫描文档的正文来寻找错误和含糊之处,而并不需要了解文本的意义.同时,静态代码分析从功能上分析源代码的

JavaScript 模块化编程(笔记)_javascript技巧

一直对JS都是一知半解,最近遇到这方面问题,所以在网上学习了一下,现在还没有完全明白,先贴出笔记; 第一章 JavaScript模块化编程 (一):模块的写法 一 原始写法// 模块就是实现特定功能的一组方法;只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块;     function m1(){         // ...     }     function m2(){         // ...     } // 上面的函数m1()和m2(),组成一个模块;使用时直

C# Socket编程笔记

看到这个题目,是不是很眼熟?在博客园里搜下,保证会发现关于这个东东的文章实在是太多了~~~真得是没有写得必要,而且我也有点懒得去琢磨字句.(看到这,肯定得来个转折的了,不然就看不到下文了,不是吗)但是,为了自己下一篇要写的文章做参考,还是有必要先补充一下socket基础知识. 注意:如果你已经接触过socket,那就没什么必要耽误时间看下去了.另外,如果发现其中任何错误,欢迎直接指出. 1.按惯例先来介绍下socket Windows中的很多东西都是从Unix领域借鉴过来的,Socket也是一样

python核心编程--笔记(不定时跟新)

的解释器options: 1.1 –d   提供调试输出 1.2 –O   生成优化的字节码(生成.pyo文件) 1.3 –S   不导入site模块以在启动时查找python路径 1.4 –v   冗余输出(导入语句详细追踪) 1.5 –m mod 将一个模块以脚本形式运行 1.6 –Q opt 除法选项(参阅文档) 1.7 –c cmd 运行以命令行字符串心事提交的python脚本 1.8 file   以给定的文件运行python脚本 2 _在解释器中表示最后一个表达式的值. 3 prin

C++多线程编程笔记

Windows系统为我们提供了相关API,我们可以使用它们来进行多线程编程. 创建线程的函数: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD SIZE_T dwStackSize, // initial stack size LPTHREAD_START_ROUTINE lpStartAddress, // thread function LPVOID lpParameter, // thread arg

C++MFC编程笔记day01 MFC介绍、创建MFC程序和重写消息处理

一.MFC概念和作用 1.全称Microsoft Foundation Class Library,我们称为微软基础类库,封闭了绝大部分的win32 Api函数,C++语法中的数据结构,程序的执行流程MFC就是一个库(动态库,静态库)MFC还是一个程序框架 2.为什么使用MFC基于框架编程,提高工作效率,减少开发周期,节约开发成本. 二.几个重要的头文件 afx.h    -绝大部分类的声明头文件 afxwin.h -包含了afx.h和windows.h afxext.h -提供了扩展窗口类的支