C-随笔
C语言的设计哲学之一: 程序员知道自己在干什么-没有安全带!
值的类型并不是值的内在本质, 而是取决于它被使用的方式
1.#include <stdio.h>在预处理器处理的时候把stdio.h中的源码读到当前文件中, 然后交给编译器
2.gets()函数会把输入的值的换行符转化为NULL字节, 来结束字符串; 遇到EOF或者发生错误返回NULL指针, 所以返回NULL要用ferror或feof检查是发生错误还是遇到EOF
3.指针属性: 地址值和指针类型. 1)地址值表示指针所标识变量的首地址; 2)指针类型告诉编译器该怎样进行接下来的访问
4.标量就是指char、int、double和枚举型等数值类型, 以及指针; 相对地, 像数组、结构体和共用体这样的将多个标量进行组合的类型, 我们称之为聚合类型(aggregate)(字符串是char类型的数组, 也就不是标量了); 而数组带下标选择的是单一元素, 若此元素为前者数值类型则是标量, 否则为聚合类型; 的一个注意区分与变量和常量间的区别(两者说的不是一个概念);使用八进制'\101'而不是数字0101来对char类型进行赋值可以很明显让人知道是字符串, 而且使用'\101'可以嵌入到字符串中
5.scanf("%d", &num); 执行时从标准输入读取, 前导空白将被跳过, printf("*%010.5d\n*", 100)-->* 00100*;第一位10表示总长度, 3表示最小数字位数(标志位:"+-0 #"), printf("*%10.2s*\n", "ABC")->"* AB*";printf使用%f输出double, 而scanf使用%lf输入double类型, 是因为printf时类型提升(短变长), float会被提升为double型, 而scanf向float和double类型中存储大不一样, 所以要用lf;scanf使用所有格式码(除了%c之外)时, 输入值之前的空白(空格、制表符、换行符等)会被跳过, 值后面的空白表示该值的结束, 因此, 用%s格式码输入字符串时, 中间不能包含空白
6.pus()函数会在字符串结尾添加一个换行符, 与gets()相反
7.C语言有四种数据类型-整型、浮点型、指针和聚合类型(数组和结构等), 所有其他的类型都是从这四种基本类型的某种组合派生而来!无布尔类型和字符串类型
8.整型包括字符、短整型、整型和长整型, 而且都分为有符号和无符号两种版本
9.break和continue只是打断最内层的循环, 不会影响到外层循环
10.goto语句, 必须定义goto到的语句, 并且在之后加上冒号":", next_do:...., 可以用goto跳出多层循环, goto next_do;
11.getchar()函数返回一个整型值, 一个原因是EOF需要的位数比字符型值提供的要多, 如果长于字符型, 会被截取\377为EOF
12.变量属性作用域、链接属性、存储类型;整型常量是能最小字节存储就最小字节存储, 若加长存储可以用L等;字符常量类型总是int
13.sizeof(int)返回int类型所占字节, sizeof(arr)返回数组arr所占总字节; 而sizeof(a = b + 1)判断表达式长度并不需要对a求值, 所以没有对a进行任何赋值
14.不论++还是--都是对变量的值的一份拷贝, 前缀在赋值之前增加变量的值, 后缀在复制之后增加变量的值, 操作符的结果不是被他们修改的变量, 而是变量值的拷贝, 认识这点非常重要; 如++a = 10;是错误的, ++优先级较高, 返回值, 值当然不能用于左操作数
15.逗号操作符, 将多个表达式分隔开来, 这些表达式自左向右逐个进行求值, 整个逗号表达式返回的值就是最后一个表达式的值
16.*p中, p代表内存中某个特定位置的地址, *操作符使机器指向那个位置; 作为左值的时候这个表达式指定要修改的位置, 作为右值的时候它就提取当前存储于这个位置的值.
17.操作符的优先级与结合性; 优先级决定了两个相邻的操作符哪个先执行, 可以依次相邻比较找出最先执行的(比如 1 + 2 + 3 * 4); 结合性就是一串操作符是从左到右依次执行还是从右到左依次执行; 优先级和结合性都与操作符有关, 而与变量或者值无关
18.没有标明存储位置, 不能作为左值
19.C根本不会对数组长度做检查, 即使索引超过数组长度也不会报错, 不论取值还是赋值!int a[5]; a[6] = 10; printf("%d", a[6]); 但这种会出现未知结果!!因为下个内存地址谁知道被谁使用呢!
20.标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较, 但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较!
21.指针减去一个整数后, 运算结果产生的指针所指向的位置在数组第一个元素之前, 那么它也是非法的!加法则稍有不同, 如果结果指针指向数组最后一个元素后面的那个内存位置仍是合法(但不该和不能对这个指针执行间接访问操作), 不过再往后就不合法了!
22.未被提前声明的函数会被编译器认为参数正确, 并且返回值为整数
23.向函数传参时传递的都是拷贝的值, 也就是值传递, 但是传递数组的时候是传递的数组指针, 也就是引用地址传递.所以函数定义时的数组参数并不需要加长度, 只需标明是数组结构即可(加上[]).
24.使用递归的时候堆栈里会产生很多函数变量, 因为堆栈的特性, 所以会覆盖原函数变量, 等本函数执行完后pop出最上变量;
25.数组名是一个指针常量, 只有在两种场合下数组不用指针常量表示-数组名用作sizeof的参数-&数组名[1]
26.指针与数组名间可以随意转换与使用, 比如int arr[10]; int *p = arr + 2; 可以直接使用p[2];C的下标和指针的间接引用是一样的!当不明白的时候就做下转换!比如此时p[-1]可以转换为*(p - 1), 即p[1]; 甚至可以写2[p], 因为其相当于*(2 + p); 但是p[11]即使超出下标范围也不会被检查!
27.数组下标引用实际执行的就是间接访问!!下标引用实际上只是间接访问表达式的一种伪装形式!!
28.彻底理解用指针和多维数组的关系
29.int matrix[2][5], *p = matrix;是错误的, 因为matrix是一个指向整数类型的指针而不是指向一个数组(a[5])的指针
30.int (*p)[10]指向数组的指针 p是指向包含10个整型元素的数组的指针
31.二维数组做参数必须指定列数 void fun(int (*mat)[10])或者void fun(int mat[][10])
32.在多维数组的初始值列表中, 只有第一维的长度会被自动计算出来
33.strlen("abcd") - strlen("abcde") >= 0;这条语句永远为真, 因为strlen返回的类型为size_t, 即unsigned int类型, 两个unsigned int类型相减根据类型自动转换翻译的依旧是unsigned int类型
34.聚合数据类型是指能够同时存储超过一个的单独数据, C提供了两种类型的聚合数据类型, 数组和结构, 数组可以通过下标访问时因为数组元素的长度相同, 但结构的成员长度可能不同, 结构变量属于标量类型;
35.结构不能包含类型也是这个结构的成员, 但是他的成员可以是一个指向这个结构的指针
36.注意结构变量的边界对齐, sizeof(结构变量)返回的值中包含了结构中浪费的内存空间
37.一个联合的所有成员都存储于同一个内存位置, 通过访问不同类型的联合成员, 内存中相同的位组合可以被解释为不同的东西.
union { float f; int i; } fi; fi.f = 3.1415926; printf("%d\n", fi.i);
38.
typedef struct{ char *name; short sex; short age; } stu, *stup;
*stup最好的理解方式为类似int *p中把int替换为(struct{...}), 声明一个指向struct类型的指针;
39.malloc、calloc、realloc和free维护一个可用内存池, 当一个程序另外需要一些内存就调用alloc系列函数从内存池中提取一块连续的内存, 并返回一个指向这块内存的指针, 如果内存池为空则返回NULL, 所以必须对alloc系列函数返回的值进行检查确定非NULL, 返回的void * 类型的指针可以被转换为任何类型的指针, 可以制作动态大小的数组
void *malloc(size_t size);
void *calloc(size_t num_elements, sizet element_size);返回内存指针前把内存中的数值初始化为0
void realloc(void *ptr, size_t new_size);修改已经分配的内存块(ptr)的大小, 如果比原来大就将新加的内存添加到原来内存之后, 小则删减后面部分, 如果原先内存不能改变则新创建一块内存, 所以realloc之后就不能使用原来的指针, 应该使用realloc返回的指针
void free(void *pointer);
40.经典的使用malloc分配内存方法
alloc.h
#include <stdlib.h> #define malloc /*注意此处为空 不能直接调用malloc*/ #define MALLOC(num, type) (type *)alloc((num) * sizeof(type)) extern void *alloc(size_t size);
接口
#include <stdio.h> #include "alloc.h" #undef malloc void *alloc(size_t size) { void *new_mem; new_mem = malloc(size); if (new_mem == NULL) { exit(1); } return new_mem; }
实现
#include "alloc.h" void function() { int *new_memory; new_memory = MALLOC(25, int); }
41.内存释放一部分是不允许的, 比如想用free(p + 5)释放不被允许;动态分配的内存必须整块一起释放, 但是realloc函数可以缩小一块动态分配的内存, 有效地释放它尾部的部分内存
42.define时左边常量不允许出现空格 否则会被认为是后个语句, 注意后者替换时括号的使用 宏定义语句中可以包含运算符"#"(将一个宏的参数不要计算而是把变量名转换为字符串字面量, 如#define PRINTF_INT(x) printf(#x "=%d\n", x))或者"##"(连接符),#x 会被替换为字符串 "x"(注意带引号),可以这样使用 printf("name" "John")
比如
#define STR(x) #x int main(int argc, char** argv) { printf("%s\n", STR(It's a long string)); // 输出 It's a long str return 0; }
#define PHP_FUNCTION ZEND_FUNCTION #define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name)) #define ZEND_FN(name) zif_##name #define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS) #define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, \ zval *this_ptr, int return_value_used TSRMLS_DC PHP_FUNCTION(count); // 预处理器处理以后, PHP_FUCNTION(count);就展开为如下代码 void zif_count(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC)
43.define函数与普通函数的区别为, 宏定义可以用于任何类型, 比如#define MAX(a,b) ((a) > (b) ? (a) : (b)); #define ECHO(s) (ges(s), puts(s))、#define ECHO(s) {gets(s); puts(s);}#define指令只能是一行 如果是换行则需要在行末把换行符转义(即加"\"), 一般的都会用do while来代替if语句, 因为";"不能乱加, 否则在if不加花括号的情况下很容易打乱if结构
#define ALLOC_ZVAL(z) \ do { \ (z) = (zval*)emalloc(sizeof(zval_gc_info)); \ GC_ZVAL_INIT(z); \ } while (0)
44.预处理命令后都不需要";", 常用预处理命令#include(可以文件嵌套, 注意区分两种搜索顺序, '/usr/include路径或当前路径'), #define, #undef, #if, #elif, #else, #endif, #ifdef, #ifndef,(#ifdef, #ifndef都是以#endif结尾) #error(编译程序(预处理阶段), 只要遇到#error就会生成一个编译错误提示消息(#error后跟的错误信息, 不用双引号), 并停止编译), #line(重新设定行号和文件名, 即修改__FILE__、__LINE__, 以后的__FILE__、__LINE__也是从当前所置值开始计算 #line 24 "a.txt", 在把其他语言解析为C语言时常用), #pragma
45.预处理器定义的符号__FILE__, __LINE__, __DATE__, __TIME__, __STDC__(编译器遵循ANSIC为1, 否则为0)
46.perror(存在于stdio.h)将错误输出到stderr;exit(存在于stdlib.h)返回错误码给操作系统(Linux下用$?查看)
perror(char const *s)用来将上一个函数发生错误的原因输出到标准设备(stderr) 参数s所指的字符串会先打印出, 后面再加上错误原因字符串, 此错误原因依照全局变量errno的值来决定要输出的字符串, 在库函数中有个errno变量, 每个errno值对应着以字符串表示的错误类型, 当你调用"某些"函数出错时, 该函数已经重新设置了errno的值, perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。
47.计算机拥有大量不同设备, 很多都与I/O操作有关, CD-ROM驱动器, 软盘和硬盘驱动器, 网络连接, 通信端口和视频适配器等都是这类设备, 每种设备具有不同的特性和操作协议, 操作系统负责这些不同设备的通信细节, 并向程序员提供一个更为简单和统一的I/O接口.ANSI C进一步对I/O的概念进行了抽象, 就C程序而言, 所有的I/O操作只是简单的从程序移进或者移出字节的事情, 因此, 好不惊奇的是, 这种字节流被称为流(stream), 程序员只要关心创建正确的输出字节数据, 以及正确的届时从输入读取的字节数据, 特定I/O设备的细节对程序员是隐藏
48.绝大多数流失完全缓冲的(fully buffered), 这意味着读取和写入是从一块被称为缓冲区(buffer)的内存区域来回复制数据, 从内存中复制数据是非常快的, 用于输出流的缓冲区只有当它写满时才会刷新(flush, 物理写入)到设备或者文件中, 一次性把写满的缓冲区写入和逐片把程序产生的输出分别写入相比效率更高, 类似, 输入缓冲区当它为空通过从设备或文件读取下一块较大的输入, 重新填充缓冲区; 使用标准输入或输出时, 这种缓冲可能会引起混淆, 所以只有操作系统断定他们交互的设备没有关联时才会进行完全缓冲.一个常见的策略就是把标准输入和输出联系到一起《 就是当请求输入时同时刷新输出缓冲区, 这样, 在用户必须进行输入之前, 提示用户进行输入的信息和以前写入到输出缓冲区中的内容将出现在屏幕上。
47.可以使用fflush(stdout/stdin)迫使缓冲区刷新, 不管是否已满;
原型: int fflush(FILE *stream); stdin刷新标准输入缓冲区, 把输入缓冲区里的东西丢弃【非标准】, stdout刷新标准输出缓冲区, 把输出缓冲区里的东西打印到标准输出设备上, printf后面加上fflush(stdout)可提高打印效率
返回: 返回值为0表示成功, 返回值为EOF表示错误
48.FILE是一个数据结构, 用来访问一个流, 每个流都有一个FILE与它关联, 为了在流上执行操作, 可以调用一些合适的函数, 并向它们传递一个与这个流相关联的FILE参数, 流通过fopen函数打开(可以打开文件或设备), 为了打开一个流你必须要指定需要访问的文件或设备, 以及其访问方式, fopen会验证文件或设备是否存在, 并初始化返回FILE *结构, 系统必须为每个ANSI C程序提供至少三个流(stdin, stdout, stderror), 他们都是一个指向FILE结构的指针(参考47, 48好好理解)
49.为每个文件活动文件声明一个指针变量, 其类型为FILE *, 这个指针指向这个FILE结构, 当它处于活动状态时由流使用
50.IO函数以三种基本的形式处理数据, 单字符/字符串/二进制数据, 对于每一种数据都有一组特定的函数对它们进行处理 -- ungetc(int ch, stdin); 把字符压回标准输入, 下次读的时候类似栈先进后出
ungetc('a', stdin); ungetc('b', stdin); printf("%c\n", getchar()); printf("%c\n", getchar());
结果
b
a
51.标准流I/O不需要打开或者关闭
52.不论以何种方式打开(rwa), 数据只能从文件的尾部写入!!
53."a+"表示该文件打开用于更新, 并且流既允许读也允许写. 但是如果你已经从该文件读了一些数据, 那么向它写入数据之前, 你必须调用一个文件定位函数(fseek, fsetpos, rewind), 在你向文件写入一些数据之后, 如果你又想从该文件读取一些数据, 你首先必须调用fflush或者文件定位函数
54.perror的使用方法
#include <stdio.h> int main(int argc, char **argv) { FILE *input = fopen("./a.txt", "r"); if (input == NULL) { perror("./b.txt"); exit(EXIT_FAILURE); } return 0; }
55.fclose(FILE *f)在关闭之前刷新缓冲区, 如果执行成功返回0, 否则返回EOF
56.输出缓冲区数据显示在屏幕上的条件
1.遇到\n
2.函数结束了
3.输出缓冲区满了
4.fflush(stdout)
测试:
printf("你好");
for(;;);
以上程序不会打印
57.编译器每次只能处理一个文件, 所以就可以理解为什么只需声明而不用见到定义!
58.连接器把编译器生成的目标文件看成一组外部对象组成的, 每个外部对象代表着机器内存的某个部分, 并通过一个外部名称来识别, 因此程序中的每个函数和每个外部变量若没有被声明为static, 就都是一个外部对象!
59.连接器载入目标模块和库文件, 处理外部对象命名冲突, 生成载入模块(可执行文件)
60.
int a;如果出现在所有函数体之外, 则被称为外部对象a的定义!这说明a是一个外部变量, 同时为a分配空间
extern int a;这个语句仍然说明a是一个外部对象, 但是由于有extern关键字, 就显式的说明了a的存储空间是在程序的其他地方分配的, 从连接器的角度来看就是一个队外部变量a的引用, 而不是对a的定义
//下面的函数在外部变量random_seed中保存了整形参数n的一份拷贝
int random_seed;
void srand(int n) {
extern int random_seed;
random_seed = n;
}
每个外部对象都必须在程序某个地方定义!所以, 一个程序中若包括了语句 extern int a; 那么这个程序就必须在别的地方包括语句 int a; 这两个语句可以在同一个源文件中也可以位于不同源文件中!如果同一个外部变量的定义不止一次, 大部分系统会拒绝接受该程序!两个具有相同名称的外部对象实际上代表的是同一对象!
61.如果是用#include一个头文件, 在编译阶段则相当于一个文件, 所以不能出现定义重复, 即使extern、staic, 可以在连接的时候使用, 比如以下情况
a.c
#include <stdio.h> extern int a; //说明a是定义在其他程序中的 int main(void) { printf("%d\n", a); return 0; }
b.c
int a = 10; //此处不能加extern 因为这才是最初始定义
执行 gcc a.c b.c 成功!
62.时刻记住include是在编译阶段, extern等连接属性是在连接阶段使用的, 仔细考虑下变量的连接属性
63