《C和指针》读书笔记

第一章

1.
#include 
#define EXIT_SUCCESS 1
#define EXIT_FAILURE 0

2.
在C语言中用/*和*/注释代码并不是十分安全(不允许嵌套),要从逻辑上删除一段C代码,最好的办法是使用#if指令。
#if 0
	statements
#endif

3.
scanf:使用所有格式码(除了%c之外)时,输入值之前的空白(空格、制表符、换行符等)会被跳过,值后面的空白表示该值的结束。


第二章

1.
三字母词(在有些环境下有用到):??( --> [        ??) --> ]        ??! --> |        ??< --> {       ??> --> }        ??' --> ^       ??= --> #        ??/ --> \        ??- --> ~
printf("??=\n");			//输出#
在编译时如果不加-trigraphs会出现警告:trigraph ??= ignored, use -trigraphs to enable [-Wtrigraphs].

2.
makefile文件和make工具!(另作解释)


第三章

1.
变量三属性:作用域(文件,函数,代码块和原型作用域),链接属性(external、internal和none)和存储类型(普通内存、运行时堆栈和硬件寄存器)。

2.
枚举类型的值,实际上为数字。可以对枚举的符号名显示地指定一个值,后面枚举变量的值在此基础上加1。

3.
const int a;			//等价于int const a;

4.
如何解决不同机器缺省整型长度不同的问题:声明整型变量名,使变量的类型必须有一个确定的长度(如int 8、int16、int32)。对于你希望成为缺省长度的整数,根据它所能容纳的最大值,使用类似defint8、defint16或defint32这样的名字。然后为每台机器创建一个名为int_size.h的文件,它包含一些typedef声明,为你创建的类型名字选择最合适的整型长度。
//32位机器
typedef signed char 	int8;
typedef short int 	int16;
typedef int 		int32;
typedef int 		defint8;
typedef int 		defint16;
typedef int 		defint32;
//16位机器
typedef signed char 	int8;
typedef int 		int16;
typedef long int 	int32;
typedef int 		defint8;
typedef int 		defint16;
typedef long int 	defint32;


第四章

1.
在while和do循环中,下一次循环开始的位置是表达式测试部分。但在for循环中,下一次循环开始的位置是调整部分。

2.
switch语句中的default与其他互斥,放在哪里无所谓。

3.
跳出多层循环的方法:
a.使用goto语句。
b.设置一个状态标志,在每个循环中都对它进行测试。
c.把所有循环放到一个单独的函数里,当灾难降临到最内层的循环时,使用return语句离开这个函数。


第五章

1.
//计数一个值中值为1的位的个数
int count_one_bits(unsigned value)
{
	int ones=0;
	while(value)
	{
		ones++;
		value&=(value-1);
	}
	return ones;
}

2.
左移操作,右边空出来的位均用0补齐。
右移操作分为两类:逻辑右移和算数右移。逻辑右移是左边移入的位用0填充,算术移位是左边移入的位由原先该值的符号位决定,符号位为1的移入的位均为1,符号位为0的移入的位均为0。

3.
int ch;
while((ch=getchar())!=EOF)
	...
getchar()函数返回整型值(EOF需要的位数比字符型值所能提供的位数要多)。
ch用int不用char:把getchar()的返回值存储于char中将导致它被截短,然后这个被截短的值被提升为整型并与EOF进行比较。当在使用有符号字符集的机器上运行时,如果读取了一个值为\377的字节时,循环会终止,因为这个值截短再提升之后与EOF相等(\377截短再提升后为-1)。当在使用无符号字符集的机器上运行时,这个循环永远不会终止(非负数不可能与-1相等)。

4.
sizeof(a=b+1);		//判断表达式的长度并不需要对表达式进行求值,并没有向a赋值。

5.
++a=10;			//错误,++a的结果是a值的拷贝,并不是变量本身,你无法向一个值进行赋值。
前缀和后缀++,--操作符都复制一份变量值的拷贝。前缀操作符在进行复制之前改变变量的值,后缀操作符在进行复制之后才改变变量的值。这些操作符的结果不是被它们所修改的变量,而是变量值的拷贝。

6.
int a=2;
int b=a+--a;		//b可能为3或2!a先求还是--a先求并不知道!
优先级只对相邻操作符的执行顺序起作用。操作符优先级规则要求自减运算在加法运算之前进行,但我们并没有办法得知加法操作符的左操作数是在右操作数之前还是之后进行求值。


第六章

1.
两个指针相减的结果类型是ptrdiff_t,它是一种有符号整数类型。

2.
标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。


第七章

1.

如果没有关于调用函数的特定信息,编译器便假定在这个函数调用时参数的类型和数量是正确的。它同时会假定函数将返回一个整型值。对于那些返回值并非整型的函数而言,这种隐式认定常常导致错误。

int func();			//旧式风格的声明(只给出func函数的返回类型,会匹配有参数的)
int func(void);			//一个没有参数的函数的原型应该写成这个样子。


2.

可变参数列表:

头文件stdarg.h声明了一个类型va_list和三个宏——va_start、va_arg和va_end。

va_list:用来保存宏va_start、va_arg和va_end所需信息的一种类型。

va_start:第一个参数是va_list变量的名字,第二个参数是省略号前最后一个有名字的参数(初始化过程把va_list变量设置为指向可变参数部分的第一个参数)。

va_arg:两个参数,va_list变量和参数列表中下一个参数的类型(返回这个参数的值,并使va_list变量指向下一个可变参数)。

va_end:

在C语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“缺省参数类型提升“。该规则同样适用于可变参数函数——对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。char、short和float类型的值实际上将作为int或double类型的值传递给函数(一定要正确指定这些类型)。


例子:printf函数的实现。格式字符串不仅指定了参数的数量,而且指定了参数的类型。


//计算指定数量的值(int)的平均值
#include 
float average(int n_values,...)
{
	va_list p;
	int i;
	float sum=0;
	va_start(p,n_values);			//使p指向可变参数部分的第一个参数!
	for(i=0;i



第八章

1.

在绝大多数表达式中,数组名的值是指向数组第一个元素的指针。这个规则只有两个例外:sizeof返回整个数组所占用的字节而不是一个指针所占用的字节。单目操作符&返回一个指向数组的指针,而不是一个指向数组第一个元素的指针的指针。

int a[10];
printf("%d %d\n%d %d\n",a,&a,a+1,&a+1);//a和&a的值一样(a是首元素的开始地址,&a是整个数组的开始地址)!但是a+1的偏移值为4,而&a+1的偏移值为40!
int *p1=a;
int (*p2)[10]=&a;

2.

int a[10];
a[2]和2[a]是否相等?			//a[2]等价于*(a+(2)),2[a]等价于*(2+(a)),所以两者相等。但不要这样用!!


3.

char message1[]="hello";		//"hello"初始化一个字符数组的元素!
char *message2="hello";			//"hello"一个字符串常量!
message1[0]='a';			//可以!
message2[0]='a';			//非法!

当字符串常量用于初始化一个字符数组时,它就是一个初始化列表。在其他任何地方,它都表示一个字符串常量。


4.

char const *keyword[]={"do","for","if",NULL};		//在表的末尾增加一个NULL指针。这个NULL指针使得在搜索这个表时能检测到表的结束。



第九章

1.

strlen()函数返回size_t(定义在,无符号整数类型)。使用时注意!

if(strlen(x)>=strlen(y))
	...
if(strlen(x)-strlen(y)>=0)			//错误,永远为真!
	...


2.

//字符串函数:
char *strncpy(char *dst,char const *src,size_t len);		/*它总是正好向dst写入len个字符。如果strlen(src)的值小于len,dst数组就用额								外的'\0'字节填充到len长度。如果strlen(src)的值大于或等于len,那么只有len								个字符被复制到dst中。注意!它的结果将不会以'\0'字节结尾。*/

char *strncat(char *dst,char const *src,size_t len);		//它最多向目标数组复制len个字符(再加一个结尾的'\0'字节)

int strncmp(char const *s1,char const *s2,size_t len);

char *strchr(char const *str,int ch);				//第一次出现的位置。
char *strrchr(char const *str,int ch);				//最后一次出现的位置。

char *strpbrk(char const *str,char const *group);		//查找任何一组字符第一次在字符串中出现的位置。

char *strstr(char const *s1,char const *s2);			//查找子串。如果第二个参数是一个空字符串,函数就返回是s1。

size_t strspn(char const *str,char const *group);		//返回str起始部分匹配group中任意字符的字符数。
size_t strcspn(char const *str,char const *group);


char *strtok(char *str,char const *seq);
//例子:
    char line[]="cccacccb";
    char *token;
    for(token=strtok(line,"c");token!=NULL;token=strtok(NULL,"c"))
    {
        printf("%s\n",token);
        printf("%d\n",token-line);
        printf("%c%c%c%c%c%c%c%c\n",line[0],line[1],line[2],line[3],line[4],line[5],line[6],line[7]);
    }
//输出:
//a
//3
//ccca ccb			第一次输出。可以看出token的值为line+3,前面3个c被跳过了,a后面的第一个c被修改成'\0'。
//b
//7
//ccca ccb			第二次输出。可以看出token的值为line+7,b前面的3个c,除了第一个被第一次调用修改成'\0',其他两个被跳过了。


3.

操作系统通过设置一个外部的整型变量errno进行错误代码报告。strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。

char *strerror(int error_number);
注意:只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改。这意味着我们不能通过测试errno的值来判断是否有错误存在。反之,只有当被调用的函数提示有错误发生时检查errno的值才有意义。查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。


4.

内存操作:

memcpy

memmove(它的源和目标操作数可以重叠。)

memcmp

memchr

memset



第十章

1.

struct Simple			//标签声明
{
    int a;
    char b;
    float c;
};
struct Simple x;


typedef struct			//typedef形式的声明
{
    int a;
    char b;
    float c;
}Simple;
Simple x;

2.

offsetof宏(定义于stddef.h)            offsetof(type,number)

type就是结构的类型,member就是你需要的那个成员名。表达式的结果是一个size_t值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。


3.

位段:首先,位段成员必须声明为int、signed int和unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。

位段提供的唯一优点是简化了源代码。这个优点必须与位段的移植性较弱这个缺点进行权衡。


4.

#define name stuff		//如果定义中的stuff非常长,它可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠。

#define宏一行写不下时,可通过‘\’换到下一行。



第十一章

1.

动态内存分配:

void *malloc(size_t size);
void free(void *pointer);
void *calloc(size_t num_elements,size_t element_size);
void realloc(void *ptr,size_t new_size);

malloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。

realloc除了在原内存块进行扩大或缩小外,还有可能出现原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针。


2.

常见的动态内存错误:

对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存、试图释放一块动态分配的内存的一部分以及一块动态内存被释放之后被继续使用。



第十二章

1.

//插入到一个有序单链表。函数的参数是一个指向链表第一个节点的指针,以及一个需要插入的新值。
int sll_insert(register Node **linkp,int new_value)
{
	register Node *current;
	register Node *new;
	while((current=*linkp)!=NULL&¤t->valuelink;
	new=(Node *)malloc(sizeof(Node));
	if(new==NULL)
		return 0;
	new->value=new_value;
	new->link=current;
	*linkp=new;
	return 1;
}

2.

省略双链表中根节点的值字段:

struct DLL_NODE;
struct DLL_POINTERS
{
    struct DLL_NODE *fwd;
    struct DLL_NODE *bwd;
};
struct DLL_NODE
{
    struct DLL_POINTERS pointers;
    int value;
}


2

#include 
void assert(int expression);

assert的作用是现计算表达式expression,如果其值为假,那么它先向stderr打印一条出错信息,然后通过调用abort来终止程序运行。



第十三章

1.

int f()[];		//非法,函数只能返回标量值,不能返回数组。
int f[]();		//非法,数组元素必须具有相同的长度,但不同的函数显然可能具有不同的长度。
int (*f[])(); 		//f是一个数组,数组元素的类型是函数指针。


2.

函数指针的应用:回调函数        转移表



第十四章

1.

__FILE__		//进行编译的源文件名
__LINE__		//文件当前行的行号
__DATE__		//文件被编译的日期
__TIME__		//文件被编译的时间
__STDC__		//如果编译器遵循ANSI C,其值就为1,否则未定义

(双下划线!)__FILE__和__LINE__在确认调试输出的来源方面很有用处。__DATE__和__TIME__常常用于在被编译的程序中加入版本信息。__STDC__用于那些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译。


2.

printf("Hello"" world!");

相邻的字符串常量会被自动连接为一个字符串。

3.

在那些对表达式求值的宏中,每个宏参数出现的地方都应该加上括号,并且在整个宏定义的两边也加上括号。

#define SQUARE(x) x*x
int a=5;
printf("%d\n",SQUARE(a+1));		//a+1*a+1输出11!

#define DOUBLE(x) (x)+(x)
int a=5;
printf("%d\n",10*DOUBLE(a));		//10*(5)+(5)输出55!


4.

把宏参数插入到字符串常量中:

a.邻近字符串自动连接的特性。

b.#argument结构由预处理器转换为字符串常量"argument"。##操作符用于把它两边的文本粘贴成同一个标识符。


5.

宏与函数的区别:

#define MALLOC(n,type) ((type *)malloc((n)*sizeof(type)))		//宏的第二个参数是一种类型,它无法作为函数参数进行传递。
int *pi=MALLOC(25,int);


6.

#undef name		//移除一个宏定义。

如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。


命令行定义:

-Dname(定义了符号name,它的值为1)

-Dname=stuff(把该符号的值定义为等号后面的stuff)

-Uname(指定-Uname将导致程序中符号name的初始定义被忽略)


7.

条件编译:

#if    #elif    #else    #endif

#if defined(symbol)等价于#ifdef symbol

#if !defined(symbol)等价于#ifndef symbol


8.

#error ??????????		//在编译时产生一条错误信息,信息中包含你所选择的文本。
#line nuber "string"		//告诉编译器下一行输入的行号,如果它加上可选内容"string",它还将告诉编译器输入源文件的名字。
#progma				//允许编译器提供不标准的处理过程,不可移植。
#				//被预处理器简单的删除。


第十五章

1.

void perror(char const *message);		/*如果message不是NULL并且指向一个非空的字符串,perror函数就打印出这个字符串,后面跟一个分号和						一个空格,然后打印出一条用于解释errno当前错误代码的信息。*/

2.
printf("something or other");
fflush(stdout);

3.

命令行:$program < data > answer

当这个程序执行时,它将从文件data而不是键盘作为标准输入进行读取,它将把标准输出写入到文件answer而不是屏幕上。


4.

FOPEN_MAX:能够同时打开至少FOPEN_MAX个文件。

FILENAME_MAX:用于提示一个字符数组应该多大以便容纳编译器所支持的最长合法文件名。


5.

//常用模式:r w a rb wb ab
FILE *fopen(char const *name,char const *mode);
FILE *freopen(char const *filename,char const *mode,FILE *stream);
int fclose(FILE *f);

//字符I/O
int fgetc(FILE *stream);			//函数
int getc(FILE *stream);				//宏
int getchar(void);				//宏
int fputc(int character,FILE *stream);		//函数
int putc(int character,FILE *stream);		//宏
int putchar(int character);			//函数
int ungetc(int character,FILE *stream);		//改变文件的位置将丢弃任何被退回到流的字符。

//未格式化的行I/O
char *fgets(char *buffer,int buffer_size,FILE *stream);
char gets(char *buffer);					//不在缓冲区中存储结尾的换行符。
int fputs(char const *buffer,FILE *stream);
int puts(char const *buffer);					//在字符串写入之后向输出再添加一个换行符。

//格式化的行I/O
int fscanf(FILE *stream,char const *format,...);
int scanf(char const *format,...);
int sscanf(char const *string,char const *format,...);
int fprintf(FILE *stream,char const *format,...);
int printf(char const *format,...);
int sprintf(char *buffer,char const *format);

//二进制I/O。把数据写到文件效率最高的方法是用二进制写入。二进制输出避免了在数值转换为字符串过程中所涉及的开销和精度损失。
size_t fread(void *buffer,size_t size,size_t count,FILE *stream);
size_t fwrite(void *buffer,size_t size,size_t count,FILE *stream);

//刷新和定位函数
int fflush(FILE *stream);
long ftell(FILE *stream);			//返回流的当前位置。
int fseek(FILE *stream,long offset,int from);	//在一个流中定位。
//SEEK_SET从流的起始位置起offset个字节,offset必须是一个非负值。
//SEEK_CUR从流的当前位置起offset个字节,offset的值可正可负。
//SEEK_END从流的当前位置起offset个字节,offset的值可正可负。如果它是正值,它将定位到文件尾的位置。
void rewind(FILE *stream);			//将读/写指针设置回指定流的起始位置,它同时清除流的错误提示标志。
int fgetpos(FILE *stream,fpos_t *position);
int fsetpos(FILE *stream,fpos_t const *position);

//改变缓冲方式
void setbuf(FILE *stream,char *buf);
int setvbuf(FILE *stream,char *buf,int mode,size_t size);

//流错误函数
int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);

//临时文件
FILE *tmpfile(void);
char *tmpnam(char *name);

//文件操纵函数
int remove(char const *filename);
int rename(char const *oldname,char const *newname);



第十六章

1.

//整型函数
int abs(int value);
long int labs(long int value);
div_t div(int denominator,int numerator);			//quot商		rem余数
ldiv_t ldiv(long int denominator,long int numerator);

int rand(void);
void srand(unsigned int seed);				//常用每天的时间作为随机数产生器的种子:srand((unsigned int)time(NULL));

//当这个函数第一次调用时,调用srand函数初始化随机数发生器。
void shuffle(int *deck,int n_cards)
{
	int i;
	static int first_time=1;
	if(first_time)
	{
		first_time=0;
		srand((unsigned int)time(NULL));
	}
	for(i=n_cards-1;i>0;i--)
	{
		int where;
		int temp;
		where=rand()%i;
		temp=deck[where];
		deck[where]=deck[i];
		deck[i]=deck[where];
	}
}

int atoi(char const *string);
long int atol(char const *string);
long int strtol(char const *string,char **unused,int base);	//strtol和strtoul函数允许你在转换时指定基数,同时还允许访问字符串剩余部分。
unsigned long int strtoul(char const *string,char **unused,int base);

//三角函数
//双曲函数
//对数和指数函数
double exp(double x);						//e的x次幂
double log(double x);						//x的自然对数
double log10(double x);
//浮点表示形式
double frexp(double value,int *exponent);			//fraction*2^exponent=value
double ldexp(double fraction,int exponent);
double modf(double value,double *ipart);			//把一个浮点值分割成整数和小数部分
//幂
double pow(double x,double y);
double sqrt(double x);
//底数、顶数、绝对值和余数
double floor(double x);						//返回不大于其参数的最大整数
double ceil(double x);						//返回不小于其参数的最小整数
double fabs(double x);
double fmod(double x,double y);
//字符串转换
double atof(char const *string);
double strtod(char const *string,char **nuused);

//日期和时间函数
clock_t clock(void);		//返回从程序开始执行起处理器所消耗的时间(处理器时钟滴答的次数,需要除以常量CLOCKS_PER_SEC才能转化为秒)。
time_t time(time_t *returned_value);		//用一个time_t值返回当前的日期和时间
char *ctime(time_t const *time_value);		//把一个time_t值转换为人眼可读的日期和时间表示形式
double difftime(time_t time1,time_t time2);	//计算两个time_t值之间以秒为单位的时间差
struct tm *gmtime(time_t const *time_value);	//把一个time_t值转换为一个tm结构。世界协调时间
struct tm *localtime(time_t const *time_value);	//把一个time_t值转换为一个tm结构。本地时间
char *asctime(struct tm const *tm_ptr);		//asctime和strftime函数把一个tm结构转换为人眼可读的日期和时间的表达形式。
size_t strftime(char *string,size_t maxsize,char const *format,struct tm const *tm_ptr);
time_t mktime(struct tm *tm_ptr);

//非本地跳转
#include 
int setjmp(jmp_buf state);
void longjmp(jump_buf state,int value);

//信号
#include 
int raise(int sig);
void (*signal(int sig,void (*handler)(int)))(int);

//打印可变参数列表
int vprintf(char const *format,va_list arg);
int vfprintf(FILE *stream,char const *format,va_list arg);
int vsprintf(char *buffer,char const *format,va_list arg);

//执行环境
void abort(void);
void atexit(void (func)(void));
void exit(int status);
void assert(int expression);			//宏,这个原型只是说明assert的用法。#define NDEBUG预处理器丢弃所有断言。
char *getenv(char const *name);
void system(char const *command);
void qsort(void *base,size_t n_elements,size_t el_size,int (*compare)(void const *,void const *));
void *bsearch(void const *key,void const *base,size_t n_elements,size_t el_size,int (*compare)(void const *,void const *));

//locale



第十七章

1.

这些ADT(以堆栈为例子)的简单实现方法带来了三个问题:

a.只允许拥有一个堆栈(可以通过把为这些结构分配内存的操作从操纵这些结构的函数中分离出来,但这样做导致封装性的损失,增加了出错机会)。

b.无法声明不同类型的堆栈(为每种类型单独创建一份ADT函数使代码的维护变得更加困难。一个更好的办法是用#define宏实现代码,然后用目标类型对它进行扩展。不过,使用这种方法,你必须小心选择一种命名约定(还不能多次定义同一类型!)。另一种方法是通过把需要存储到ADT的值强制转换为void *。这种策略的一个缺点是它绕过了类型检查)。

c.需要避免不同ADT之间以及同种ADT用于处理不同类型数据的各个版本之间避免名字冲突(我们可以创建ADT的泛型实现,但为了正确使用它们,用户必须承担更多的责任)。


2.

使用断言检查内存是否分配成功是危险的。


3.

让二叉搜索树降序输出:修改中序遍历,先遍历右子树然后遍历左子树。



第十八章

1.

在UNIX系统中,编译器选项-S使编译器把每个源文件的汇编代码写到一个具有.s后缀的文件中。


2.

函数参数以它们在参数列表中的相反次序逐个压到堆栈中。


3.

虚拟内存:由操作系统实现,它在需要时把程序的活动部分放入内存并把不活动的部分复制到磁盘中(在UNIX系统中将磁盘这块区域称为交换空间)。

你可能感兴趣的:(C/C++)