C专家编程 总结

1 类型转换

当执行算术运算时,操作数的类型如果不同,就会发生转换,数据类型一般朝着浮点精度高、长度更长的方向转换,整数型如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned。

K&R C所采用无房户后保留原著,就是当一个无符号类型与int或更小的整型混合使用时,结果类型是无符号类型。

 

2 C语言中const并不真正表示常量。

 

3 switch语句的缺点

1)switch语句最大的缺点是它不会在每个case标签后面的语句执行完毕后自动终止。

2)由于break语句事实上跳出的是最近的那层循环语句或switch语句,所以break可能使switch语句提前跳出结束。

 

4 C语言中的符号重载

 

符号         意义
static 
在函数内部,表示该变量的值在各个调用间一直保持延续性

在函数这一级,表示该函数只对本文件可见

extern
用于函数定义,表示全局可见(属于冗余的)

用于变量,表示它在其他地方定义

void
作为函数的返回类型,表示不返回任何值

在指针声明中,表示通用指针的类型

位于参数列表中,表示没有参数

*
乘法运算符

用于指针,间接引用

在声明中,表示指针

&
位的AND操作符

取地址运算符

= 赋值符
== 比较运算符

<=

<<=


小于运算符

左移复合赋值运算符

<
小于运算符

#include指令的左定界符

()
在函数定义中,包围形式参数表

调用一个函数

改变表达式的运算次序

将值转换为其他类型(强制类型转换)

定义带参数的宏

包围sizeof操作符的操作数(如果它是类型名)

5 C语言中的优先级

优先级问题 表达式 人们可能误以为的结果 实际结果

.的优先级高于*。->操作符

用于消除这个问题

*p.f p所指对象的字段f (*p).f
对p取f偏移,作为左值,然后进行

解除引用操作。*(p.f)

[]高于* int *ap[] ap是个指向int数组的指针 int(*ap)[] ap是个元素为int左值的数组int *(ap[])
函数()高于* int *fp() fp是个函数指针,所指函数返回int,int(*fp)() fp是个函数,返回int*,int*(fp())
==和!=高于位运算符 (val&mask!=0) (val&mask)!=0 val&(mask!=0)
==和!=高于赋值符 c=getchar()!=EOF (c=getchar())!=EOF c=(getchar()!=EOF)
算术运算符高于移位运算符 msb<<4+lsb (msb<<4)+lsb msb<<(4+lsb)
逗号运算符在所有运算符中优先级最低 i=1,2 i=(1,2) (i=1),2

结合性只用于表达式中出现两个以上相同优先级的操作符的情况,用于消除歧义。事实上,你会注意所有优先级相同的操作符,它们的结合性也相同。

 

6 返回局部对象的指针

 

char * localized_time(char* filename)
{
    struct tm *tm_ptr;
    struct stat stat_block;
    char buffer[120];

    stat(filename,&stat_block);
    tm_ptr=localtime(&stat_block.st_mtime);
    strftime(buffer,sizeof(buffer,"%a %b %e %T %Y",tm_ptr);
    return buffer;
}

问题就出在最后一行,也就是返回buffer的那行。buffer是一个自动分配内存的数组,是该函数的局部变量。当控制流离开声明自动变量(即局部变量)的范围时,自动变量就会失效。这就意味着即使返回一个指向局部变量的指针,当函数结束时,由于该变量已被销毁,谁也不知道这个指针所指向的地址的内容是什么。

在C语言中,自动变量在堆栈中分配内存。当包含自动变量的函数或代码块退出时,它们所占用的内存便被回收,它们的内容肯定会被下一个调用的函数覆盖。这一切取决于堆栈中先前的自动变量位于何处,活动函数声明了什么变量,写入了什么内容等。原先自动变量地址的内容可能被立即覆盖,也可能稍后才被覆盖。

 

解决这种问题有几种方案:

1 返回一个指向字符串常量的指针。例如:

char* func() { return "Only works for simple strings";}

2 使用全局声明的数组。例如:

char *fun() {

...

my_global_array[i] = 

...

return my_global_array;

}

这个适用于自己创建字符串的情况,也很简单易用。它的缺点在于任何人都有可能在任何时候修改这个全局数组,而且该函数的下一次调用也会覆盖该数组的内容。

3 使用静态数组。例如:

char * func()

{

static char buffer[20];

...

return buffer;

}

这就可以防止任何人修改这个数组。只有拥有指向该数组的指针的函数才能修改这个静态数组。但是,该函数的下一次调用将覆盖这个数组的内容,所以调用者必须在此之前使用或备份数组的内容。和全局数组一样,大型缓冲区如果闲置不用是非常浪费内存空间的。

4 显式分配一些内存,保存返回的值。例如:

char* func(){

char *s=malloc(120);

...

return s;

}

这个方法具有静态数组的优点,而且在每次调用时都创建一个新的缓冲区,所以该函数以后的调用不会覆盖以前的返回值。它适用于多线程的代码。它的缺点在于程序员必须承担内存管理的责任。根据程序的复杂程度,这项任务可能很容易,也可能很复杂。如果内存尚在使用就释放或者出现“内存泄露”(不再使用的内存未回收),就会产生令人难以置信的Bug。

5最好使用的解决方案就是要求调用者分配内存来保存函数的返回值。为了提高安全调用者应该同时指定缓冲区的大小。

void func(char* result,int size){

...

strncpy(result,"That't be in the data segment,Bob",size);

}

buffer=malloc(size);

func(buffer,size);

...

free(buffer);

如果程序员可以在同一代码块中同时进行“malloc”和“free”操作,内存管理是最为轻松的。这个解决方案就可以实现这一点。

 

7 声明是如何形成的

不合法的声明:

  • 函数的返回值不能是一个函数,所以像foo()()这样是非法的。
  • 函数的返回值不能是一个数组,所以像foo()[]这样是非法的。
  • 数组里面不能有函数,所以像foo[]()这样是非法的。

合法的声明:

  • 函数的返回值允许是一个函数指针,如: int(*fun())();
  • 函数的返回值允许是一个指向数组的指针,如: int(*foo())[];
  • 数组里面允许有函数指针,如int(*foo[])()
  • 数组里面允许有其他数组,所以你经常看到int foo[][]

8 typedef的使用

typedef与define的区别:

首先,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名不能这样做。如下所示:

#define peach int 

unsigned peach i; //没问题

typedef int banana;

unsigned banana i;  //错误! 非法

其次,在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。如下所示:

#define int_ptr int *;

int_ptr chalk,cheese;

经过宏扩展,第二行变为:

int * chalk,cheese;

这使得chalk和cheese成为不同的类型:chalk是一个指向int的指针,而cheese则是一个int。相反,下面的代码中:

typedef char* char_ptr;

char_ptr Bentley,Rolls_Royce;

Bentley和Rolls_Royce的类型依然相同。虽然前面的类型名变了,但它们的类型相同,都是指向char的指针。

 

//下面两个声明具有相似的形式

typedef struct fruit{ int weight, price_per_lb; } fruit; //语句1
struct veg{ int weight,price_per_lb; } veg;  //语句2

但它们代表的意思却完全不一样,语句1声明了结构表情“fruit”和由“typedef声明的结构类型”fruit“,其实际效果如下:

struct fruit mandarin;   //使用结构标签fruit

fruit mandarin;   //使用结构类型fruit

语句2声明了结构标签veg和变量veg,只有结构标签能够在以后的声明中使用,如

struct veg potato;

如果试图使用veg  cabbage这样的声明,将是一个错误。这有点类似下面的写法:

int i;

i j;

 

9 声明和定义的区别

记住,C语言的对象必须有且只有一个定义,但它可以有多个extern声明。

定义是一种特殊的声明,它创建了一个对象;声明简单地说明了在其他地方创建的对象的名字,它允许你使用这个名字。

 

定义   只能出现在一个地方         确定对象的类型并分配内存,用于创建的对象。例如,int my_array[100];

声明   可以多次出现          描述对象的类型,用于指代其他地方定义的对象(例如在其他文件里)例:extern int my_array[];

 

区分定义和声明

只要记住下面的内容即可分清定义和声明:

声明相当于普通的声明:它所声明的并非自身,而是描述其他地方的创建的对象。

定义相当于特殊的声明:它为对象分配内存。

 

extern对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行。由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。对于多维数组,需要提供除最左边一维之外其他维的长度——这就给编译器足够的信息产生相应的代码。

 

10 数组和指针的访问

C语言引入了”可修改的左值“这个术语,它表示左值允许出现在赋值语句的左边,这个奇怪的术语是为与数组名区分,数组名也用于确定对象在内存中的位置,也是左值,但它不能作为赋值的对象。因此,数组名是个左值但不是可修改的左值。

 

左值:出现在赋值符左边的符号有时被称为左值。

右值:出现在赋值符右边的符号有时则被称为右值。

编译器为每个变量分配一个地址(左值)。这个地址在编译时可知,而且该变量在运行时一直保存于这个地址。相反,存储于变量中的值(它的右值)只有在运行时才可知。如果需要用到变量中存储的值,编译器就发出指令从指定的地址读入变量值并将它存于寄存器中。

这里的关键之处在于每个符号的地址在编译时可知。所以,如果编译器需要一个地址(可能还需要加上偏移量)来执行某种操作,它就可以直接进行操作,并不需要增加指令首先取得具体的地址。相反,对于指针,必须首先在运行时取得它的当前值,然后才能对它进行解除引用的操作(作为以后进行查找的步骤之一)。

相反,如果声明extern char *p,它将告诉编译器p是一个指针,它指向的对象是一个字符。为了取得这个字符,必须得到地址p的内容,把它作为字符的地址并从这个地址中取得这个字符。指针的访问要灵活很多,但需要增加一个额外的提取,如图所示:

11 数组和指针的其他区别

比较数组和指针的另外一个方法就是对比两者的特点。

数组和指针的区别

指针          数组

保存数据的地址

保存数据               

间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。

如果指针有一个下标[I],就把指针的内容加上I作为地址,从中提取数据


 直接访问,a[I]只是简单地从a+I为地址取得数据

通常用于动态数据结构 通常用于存储固定数目且数据类型相同的元素
相关的函数为malloc() free()  隐式分配和删除
通常指向匿名数据 自身即为数据名

定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义时赋给指针一个字符串常量进行初始化。例如,下面的定义创建了一个字符串常量(为其分配了内存):

char *p="breadfruit";

注意只有对字符串常量才是如此。不能指望为浮点数之类的常量分配空间,如:

float *pip=3.141;  //错误

初始化指针时所创建的字符串常量被定义为只读的。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。

 

数组也可以用字符串常量进行初始化:

char a[]="gooseberry";

与指针相反,由字符串常量初始化的数组是可以修改的。

 

 12 UNIX中的堆栈段和MS-DOS中的堆栈段

在UNIX中,当进程需要更多空间时,堆栈会自动生长。程序员可以想象堆栈是无限大的。这是UNIX胜过其他操作系统如MS-DOS的许多优势之一。在UNIX的实现中一般使用某种形式的虚拟内存。当试图访问当前系统分配给堆栈的空间之外时,它将产生一个硬件中断,称为页错误。处理页错误的方法有好几种,取决于对页的引用是否有效。

在正常情况下,内核通过向违规的进程发送合适的信号(可能是段错误)来处理对地址的引用。在堆栈顶部的下端有一个称为red zone的小型区域,如果对这个区域进行引用,不会产生失败。相反,操作系统通过一个好的内存块来增加堆栈段的大小。

在DOS中,在建立可执行文件时,堆栈的大小必须同时确定,而且它不能在运行时增加,如果你猜测错误,需要的堆栈空间大于所分配的空间,那么你和程序都会迷失。如果设置了检查选项,就会收到STACK OVERFLOW(堆栈溢出)消息。

 

13 返回局部指针的两种不同情况。

 先看下面的程序:

#include<iostream>
using namespace std;

char* test2()
{
    char p[] = "hello world";
    return p;
}
char* test3(){
    char *p = "hello world";
    return p;
}
int main()
{
    test2();
    test3();
}

其中,test2中p是一个数组,分配在栈空间,在栈空间存放字符串。当是数组p,则函数会将字符串常量的字符逐个复制到p数组里面,返回p则是返回数组p,但是调用函数结束后p被销毁,里面的元素不存在了。

但是在test3中p是一个指针,则p指向存放字符串常量的地址,返回p则是返回字符串常量地址值,调用函数结束字符串常量不会消失(是常量)。所以返回常量的地址不会出错。

虽然都是返回局部变量的指针,但是test2结果会有问题,但是test3不会。

时间: 2024-10-04 05:25:37

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,而其他的编程语言好像没有这类书.这里有一

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

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

C专家编程 笔记

C语言中的符号重载 C语言非常的简洁, 以至于不愿意用太多的符号, 这样有很多符号在不同的地方有不同的含义 这样会让用户很困惑, 这是c的语言特性, 也是设计上的一些失误 static     在函数内部,表示该变量的值在各个调用间一直保持延续性: 对于函数,表示该函数只在本文件中可见 extern  用于变量,表示该变量在其它地方定义: 用于函数定义, 表示全局可见(属于冗余的) void      用于参数列表中,表示该函数参数为空,如int main(void): 用于返回值,表示该函数返

《JavaScript专家编程》——1.2 对象概述

1.2 对象概述 JavaScript是由Brendan Eich创建的一种面向对象编程(OOP)语言,当时他还在Netscape公司工作,花了几周的开发时间就发布了.虽然JavaScript的名字中有个"Java",但它实际上跟Java语言没什么关系.在InfoWorld的一篇对Eich的采访稿中,他解释了JavaScript命名的由来: InfoWorld:据我所知,JavaScript开始的时候叫Mocha,后来改名叫LiveScript,在 Netscape和Sun合并以后才叫

《JavaScript专家编程》——1.3 小结

1.3 小结 对象是持有零个或多个属性的包. 对象的属性要么是基本类型,要么是复杂类型.对象可以持有它们自己的基本类型的拷贝,但仅能持有复杂类型的引用.出于这个原因,JavaScript的属性要么传引用,要么传值. 对象的属性可以有标记符,可以在修改对象时,控制对象的行为和能力. 对象可以通过以下三种方式之一创建. 使用字面量语法'{}'. 联合使用new运算符和构造函数,例如'new Foo()'. 使用内置的Object.create()函数. JavaScript是一种基于原型的语言,对象

《JavaScript专家编程》——第1章 对象和原型 1.1鸟瞰JavaScript

第1章 对象和原型 练习不会造就完美,只有使用最佳的方法来练习才能造就完美. --Vince Lombardi 对专家来说,把JavaScript的核心概念讲上3章似乎有点多,毕竟这些是语言最基本的组成部分.我的主张是,有的人虽然不能读写,但可以说话.就像有的开发人员对JavaScript的基本功能很熟悉,但对里面那些复杂的东西可能就没那么了解了. 本书的目标是像明灯一样照亮语言中那些晦涩的角落.里面包含的很多概念你可能已经试着学习过了,甚至可以假设你已经理解了.这里可以想象一下:你正降落到你大

《JavaScript专家编程》——9.5 小结

9.5 小结 本章介绍了JavaScript代码质量的必要性及其原因.一个程序的质量往往会影响程序员维护.改进甚至是否有能力完全了解源代码.质量差的代码通常被称为技术债,剥夺了项目的时间和资源,而这些时间和资源用在其他地方能更好地发挥作用.然而,编程往往是一门跨越了艺术和科学之间的学科,这使得对质量的定义变的更为复杂.而且,质量又是从主观和客观上的双重度量. 有这样一种观点认为,一个人所在时代的文化,以及他们的个人经历都会对质量产生影响.这种观点将质量描述为具有"内在卓越"的东西,只有

《JavaScript专家编程》——导读

**前言**在我看来,好的技术书籍是磁带.藏宝图和现场札记内容的混合体.本书就是我灌注了很多心血而将这些不同形式融为一体的一本书. 老一辈的人还记得,磁带内容是由很多歌曲组成的.这些磁带经常被作为礼物送给朋友.恋人.人们会挑选一些个人喜欢的歌,或者围绕某个主题组织在一起的歌,录入这盘磁带中.通常,当听磁带的人听到这些歌时,这些歌就会勾起人们对录制者的记忆.这本书就是一盘我录给你们的JavaScript方面的磁带.这些章节包括JavaScript中我喜欢的一些方面,也包括不容易被理解的主题,因为这

《JavaScript专家编程》——第9章 代码质量 9.1 定义代码质量

第9章 代码质量 质量不是一种行为,而是一种习惯. --Aristotle 写高质量的JavaScript是什么意思?质量能度量吗?还是说它是一个主观感受,类似于美和艺术的柏拉图式的理想?程序员往往会在质量的主观和客观理解之间摇摆不定.他们提出了诸如软件工艺的概念,这是一种用类似手工艺的方法来编写软件的方式.软件工匠常被这样描述:他们拥有超群的技术,总是能将工作提炼为基本的.本质的部件.这样一个工匠在电气上被称为摇滚明星程序员.这基于两个标准,一是这个人具有如同艺术家一样的独特天赋,二是他工作的

《JavaScript专家编程》——9.2 如何度量质量

9.2 如何度量质量 你正在为质量寻找一个可用的定义,但因为它们涉及到编程,所以需要首先考虑它的各个方面.通常会将这些方面表示为软件度量: 软件度量是一个软件在某些属性或其规格上的度量.由于定量度量在所有科学中都是必不可少的,因此计算机科学从业者和理论家通过不断努力,将类似的方法引入软件开发中.我们的目标是获得客观的.可重复的和可量化的度量,这可能包含很多有价值的实践,包括进度规划.预算规划.成本估算.质量保证测试.软件调试.软件性能优化以及人员任务分配优化. 我已经努力总结出六个指标,来度量代