C语言核心技术笔记(二)

C语言核心技术(原书第2版)彼得·普林茨(Peter Prinz)托尼·克劳福德(Tony Crawford).

第六章:语句

  1. 表达式语句即后面跟着一个分号的表达式,作用是计算实现某个功能。其本身类型与值并没有意义,语法上 10; a < b; 都是正确的,只是没有实际作用。函数调用也是一个语句,如果不需要返回值,可以显式的加上(void)类型转换符,当然不加也是可以的。
  2. scanf 返回值:返回 实际输入字符串与 语句中 格式字符串 匹配的次数,即有多少项被成功赋值,如果没有匹配就返回0,如果遇到输入流尾端或错误就返回 -1(通常表现为EOF,是-1的宏定义)。(文件/输入结束符,windows 下按 Ctrl + z + Enter, Linux 下按 Ctrl + D) 其实printf 也有返回值,返回实际输出的字符的长度,即使是一个整数,输出的也是整数的长度。如 输出123 返回值为3.
  3. switch - case 中每个case常量都必须唯一,dafault可以放在switch 语句体内任何位置,语句执行顺序从第一个case标签开始,因此放在第一个case 标签前的语句不会被执行;switch 跳转到某个符合条件的case 后,就不再判断case,而是顺序执行之后的所有语句,除非break,否则switch语句块中后面的语句都有可能被执行。
  4. goto 只用于跳转到同一函数的另一条语句。标签拥有独立的命名空间,不会与同名变量/类型冲突。标签可以放在任何语句前面,对语句本身没有任何影响;同一条语句也可以放置多个标签。如果跳转会跨越变量声明与初始化语句,就不应采用这种跳转形式;此外如果跳转跨越了可变长度数组的定义,而跳转到作用域内部则这种跳转是非法的。
  5. 进行非局部跳转可以利用标准宏setjmp() 和标准函数longjmp(). 宏setjmp()在程序中设置一个地点将程序流的必要信息存储起来,在调用longjmp()时,可以在任何时刻返回到该地点继续执行。相关细节有待补充。

第七章 函数

  1. extern(外部链接标识符) 不严格需要显式定义,因为函数默认存储类型就是extern的。即一般的函数定义,除非定义了static 或 inline,能够在任何源代码文件中使用,只需在使用前先进行声明即可。

  2. 函数的形参可以认为是作用域在函数块内的普通局部变量,发生函数调用时会先创建这些形参,然后用对应的实参进行初始化,而不会对原来的实参有任何影响。可以使用register存储类修饰符来声明函数参数,以便建议编译器尽量快的获取该变量。

  3. 使用数组来声明函数参数时,可以使用 类型 名称 [ ] 的形式,方括号中添加的任何常量表达式都会被忽略。当然可以使用指针的形式来声明,不过数组格式的声明可读性更好,而且允许在声明时将类型限定符const, volatile, restrict 放在方括号内来允许声明具有限定符指针类型的参数;此外还允许将存储类修饰符static连带整数常量表达式一起放在方括号内,表明函数调用时数组内元素数量至少等于该常量表达式。

    int func( long array[const static 5] )
    { /* ... */ }
    
  4. main 函数的参数 argc(argument count)的值为命令行中启动该程序的字符串的数量,包括程序本身的名称, argv(arguments vector)是一个char指针数组,每个指针都独立的指向命令行中的每个字符串,argv[0] 表示程序名称(若运行环境不支持程序名称,则为空),argv[argc]表示的是空指针。

  5. 函数声明中参数的标识符是可选的,编译器会忽略这些参数名,更多的是像注释一样用来编程时说明参数的目的 ;对于参数是某个长度可变数组函数的声明,可以用 星号 * 来表示数组长度,如果使用的是非常量的整数表达式,编译器通常也会将其视为星号。

  6. 内联函数由于在每个使用该函数的翻译单元中都必须重复定义这个函数,因此编译器必须时刻准备好函数定义,以便及时插入内联代码;所以经常在头文件中定义内联函数。此外如果翻译单元的某个函数的所有声明都具有inline修饰符,没有extern修饰符,则内联定义只针对该翻译单元,不构成外部定义,其他翻译单元可以包含该函数的外部定义。如果两种定义都能被获取,编译器会自由选择使用哪种定义。

  7. 函数修饰符 _Noreturn 用于告知编译器这类函数不需要返回,编译器会因此进行进一步优化。常见的无返回标准函数有: abort(), exit(), _Exit(), quick_exit(), thread_exit();这些函数目的是结束一个线程或整个程序的执行。longjmp()也是一个无返回函数,只不过会跳转到setjmp()语句继续执行。
    应慎重使用_Noreturn来声明函数,此外如果包含了stdnoreturn.h 头文件,也可以使用同义词noreturn 来替代。

  8. 递归在某些问题上可以提供简洁的解决方案,但在某些情况下采用循环的解决方案效率会更高。

  9. 可变参数函数必须要有至少一个强制参数,后面再接数量可变的可选参数。参数列表的格式是强制性参数在前,后面跟着一个逗号和省略号。获取可选参数列表时可以通过一个va_list对象,称为参数指针,其包含了参数信息。头文件定义为stdarg.h。
    在该头文件中处理参数指针有4个宏:

    /* lastparam 最后一个有名称参数的名称 */
    void va_start(va_list argptr, lastparam);
    /* type 刚刚被读入的参数类型 比如 int/double等,返回对应类型的值 */
    type va_arg(va_list argptr, type);
    void va_end(va_list argptr);
    void va_copy(va_list dest, va_list src);
    

    必须先调用va_start才可以使用可选参数。va_arg 得到当前argptr引用的可选参数,同时将 argptr 移动到列表的下一个参数。va_end来结束参数指针的使用。va_copy 用当前的 src来初始化参数指针 dest。一个书上的样例:

    double add( int n, ... )
    {
    	int i = 0;
    	double sum = 0;
    	va_list argptr;
    	va_start(argptr, n);
    	for (i = 0; i < n; ++i)
    	{
    		sum += va_arg(argptr, double);
    	}
    	va_end(argptr);
    	return sum;
    }
    

第八章 数组

  1. C99允许把非常量表达式作为元素数量来定义数组,前提是数组具有动态存储周期,并且不能作为结构或联合的成员。不过由于动态对象存储在栈中,因此对于较小的、临时的数组,定义长度可变数组才合理。
  2. 动态存储周期的数组元素值是没有定义的,其他情况下的元素会默认初始化成0。显式初始化可以用初始化列表: 大括号内各个元素以逗号隔开。长度可变数组在定义时不能进行初始化;静态存储周期的数组必须用常量表达式初始化,动态存储周期的数组可以使用变量。提供初始化列表中的数组可以省略数组长度。若指定了长度又在初始化列表中没有初始化器,则被初始化成0。若初始化列表长度大于数组长度,则剩下的被忽略,只不过有时编译器会发出警告。
  3. 神奇的初始化器:可以通过元素指示符将初始化器关联特定的元素,格式为:[ 整数常量表达式 ],例如:
    int a[10] = { 1, 2, [5] = 3, 4 };
    /*这里的a[0] = 1, a[1] = 2, a[5] = 3, a[6] = 4, 其他都为0*/
    
    没有元素指示符的初始化器会被关联到前一元素的下一个元素。此外也可以定义没有指定长度的数组,例如:
    int a[ ] = { [1000] = -1 };
    /* 会创建一个长度为1001的数组,其最后一项为-1,其他为0. */
    
  4. 多维数组的初始化,可以使用多个大括号的初始化列表。某个大括号内可以不全部显式初始化,未定义的部分都初始化成0;此外可以省略某些大括号,因为如果某对大括号包含了比对应数组维度元素数量还多的初始化器,则多出来的初始化器会被关联到存储序列中的下一个数组元素中。同样可以使用元素指示符来初始化特定元素,例如:
    int a [2][3][3] = { 1, [0][1][0] = 4, [1][0][0] = 7, 8 };
    //等效于下面的
    int a [2][3][3] = { {1}, [0][1] = {4}, [1][0] = {7, 8} };
    
  5. C99规定可以在函数声明中使用长度可变数组,因此对于矩阵的指针声明中,列数可以不为常量,用函数的另一个参数代替,

第九章 指针

  1. 指针定义: 类型 * [类型限定符列表] 名称 [= 初始化器]; 其中星号是声明符(名称)的一部分,类型限定符包含const, volatile, restrict 等。
  2. 所有没有显示初始化的指针 默认初始化值为 空指针。指针如果不具备动态存储周期,就必须用常量表达式来初始化。
  3. 通常来说,指针版本的函数比数组索引版本的函数具有更高效率,因为索引a[i]或者*(a + i)总是需要将a的地址加上 i * sizeof(元素类型) 来获取对应数组元素地址。指针版本可以直接累加当前地址而不是累加索引地址来减少运算量,而且是直接指向所需的元素,操作更为方便。
  4. 指针类型限定符有const, volatile, restrict; 其中const 和 volatile 可以限定指针本身的类型,或者是限定指针所指对象的类型。具体取决于限定符相对星号的位置。 restrict 修饰的指针被称为受限指针,在受限指针的生存周期内,该对象只能使用该受限指针来修改或获取。restrict 实际上是限制程序员应该保证不会用变量名称或别的指针来修改当前指针指向的对象(只是读取还是可以的)。如果这样做了可能会产生运行错误。如memcpy()函数的src 指针就有restrict 修饰符,保证 dest 与 src 所指向的内存空间没有重叠。
  5. 指向数组的指针,定义时必须用括号,否则就成了存储指针的数组了。如
    int (* arrPtr)[10] = NULL;
    
    这里的arrPtr指向了一个具有10个元素的 int 数组类型,如果分配了合适的数组地址,则 *arrPtr 可以获得该数组,(*arrPtr)[i]会获得数组索引为 i 的元素,**arrPtr 表示数组第一个元素。这种指针一般用在多维数组中,如果有 ++arrPtr,则表示移动到下一行。此外数组指针无法直接赋值,因为右操作数会被隐式的转换成指向第一个元素的指针,不过降级赋值(高赋给低)和强制类型转换都是可以的
    int a[10];
    arrPtr = (int (*)[10]) a;
    
  6. 指针数组:一个初看有问题,细看有点东西的样例:(不过这里的myStr[i]指向的都是只读内存数据,对其进行修改是不允许的,可以用const修饰)
    #define ARRAY_LEN 100
    char *myStr[ARRAY_LEN] = 
    {
    	"abcdefg.",
    	"aoeiuvbpmfdtl.",
    	"eee"
    };
    
  7. 指向函数的指针,函数名总是会被隐式的转换成函数指针。在函数调用时 (*func) 和直接使用 func 总是一样的。总是可以使用typedef定义简单的类型名称来声明函数指针数组,如:
    typedef double func_t( double, double);
    func_t *funcTable[5] = { Add, Sub, Mul, Div, pow };
    

第十章 结构、联合与位字段

  1. 一般将结构/联合/枚举类型名称的第一个字母大写,便于区分。
  2. 可以采用声明指向不完整结构的指针,以定义互相应用的结构类型。如:
    struct A { struct B *pB; /* other */};
    struct B { struct A *pA; /* other */};
    
    以上定义是合法的,除非在两者定义之外的作用域内有另一个struct B 的定义,这时结构体A中指向的就是外部定义的结构体B;可以在struct A 定义之前加上 struct B; 的声明,这样就会屏蔽外部作用域的标签。
  3. 可以使用赋值运算把整个结构对象的内容复制到另一个同类型的对象中;具有静态存储周期的对象其成员初始值为0或者NULL,动态存储类型的对象其成员值不确定。可以使用以逗号隔开的初始化器组成的初始化列表来初始化某个结构体,如果成员是结构体,则可以递归使用另一个结构体的初始化器;未被初始化的成员都被赋值为0。
  4. C99标准允许显示的将一个初始化器和某个 特定的成员关联起来,使用时必须在初始化器的等号前面加上一个成员指示符 " .member"。如果不带成员初始化符,则默认对应为前一个已被初始化成员的下一个成员,没有前一个初始化成员就对应第一个成员。例:
    struct Song aSong = { .title = "something", 123};
    
  5. 结构类型定义时声明各成员的顺序,就是对应结构对象在内存中的存储顺序。可以通过宏offsetof获得成员地址与结构对象的起始地址之间的偏差(stddef.h)。使用方式是:
    offsetof( structure_type, member)
    
    由于编译器为了方便获取成员常常对结构体进行对齐操作,以及在最后一个成员后面补充额外的字节,导致结构空间大小可能会大于所有成员的空间大小之和。无论如何,应该使用sizeof获取结构的空间大小,用宏offsetof获得结构成员的位置。可以通过编译器的特定命令选项控制是否对齐(如需要与硬件接口保持一致性时).
  6. C99规定结构体的最后一个成员可以是不完整的数组类型。这样的结构成员被称为弹性数组成员。不过弹性成员所占空间并未被计算在数组所占空间中。此外无法对弹性成员进行初始化,只有在动态分配内存的时候,可以指定弹性成员所占的空间。此外在没有指定弹性成员的其他操作(如赋值)中都会忽略弹性成员,也不会分配空间。反正不好用,尽量别用。
  7. 对于联合类型来说,可以通过初始化列表来初始化联合对象,只是列表中只有一个初始化器,C99允许在初始化器中使用成员指示符来指定某个成员进行初始化,如果没有指定就默认使用第一个;同样的没有指定的剩余成员会被初始化成0.
  8. C11支持匿名结构与联合:一个结构或联合在定义时未命名结构成员或者未指定标签名称。一个匿名结构或联合的成员,被视为包括匿名类型的结构或联合成员,就是可以通过外部结构体直接访问匿名结构体的成员,好像中间的匿名结构框架不存在一样。这种访问机制还可以递归的应用下去。
  9. 位字段是一个由具有特定数量的位组成的整数变量,如果连续声明多个小的位字段,编译器会将它们合并成一个机器字。相比于使用位运算符来处理特定位,位字段还可以利用名称来处理位。声明格式为:
    // 中括号表示这是可选字段,不是格式的一部分
    类型 [成员名称] :宽度
    
    其中类型为一个整数类型,可以是_Bool, int, signed int, unsigned int或者其他提供的类型,这些类型也可以包含类型限定符。成员名称表示是可选的,只不过声明的是无名称的位字段是无法获取的,只能用于填充。便于之后的位字段对齐。宽度是一个非负数,只有无名称的位字段其宽度可以为0。
  10. 位字段可以分配很少的空间,例子:
    struct Date {
    	unsigned int month : 4;
    	unsigned int day   : 5;
    	signed int year    : 22;
    	_Bool isDST        : 1;
    };
    
    在最少使用32位字的机器下,其对象占据的存储空间大小与一个32位的int 整数对象一样。由于位字段没有占据可寻址的内存位置,因此无法对位字段采用地址运算符(&) 或宏(offsetof)。但在其他方面位字段和结构体内的成员几乎一致。使用点或箭头运算符来获取,并且可以和int 或unsigned int 变量一样的进行算数运算。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值