关键词的秘密
数据类型
什么是数据类型? 数据类型可理解为固定内存大小的别名; 数据类型是创建变量的模子(花形的,圆形的,星形的等等);
char
1byteshort
2byteint
4byte
内存空间
---------- | char c | ---------- | short s | | | ---------- | | | int i | | | | | ----------
变量的本质
变量也是别名;比如int num
告诉编译器申请int内存的大小并取名num; 变量是实际连续存储空间的别名; 存储空间可以通过变量申请并命名存储空间; 每个变量对应的内存空间都有一个编号,即地址; 指针也是一种存储普通变量地址的变量;
类型与变量的关系
即建筑图纸与实际建筑的关系
#include <stdio.h> int main() { char c = 0; short s = 0; int i = 0; printf("%ld, %ld\n", sizeof(char), sizeof(c)); //1,1 // 变量的大小与定义他的类型相同 // 深刻理解数据类型与变量的关系 printf("%ld, %ld\n", sizeof(short), sizeof(s)); //2,2 printf("%ld, %ld\n", sizeof(int), sizeof(i)); //4,4 return 0; }
通过打印句子证明变量的本质
#include <stdio.h> typedef int INT32; typedef unsigned char BYTE; typedef struct _demo { short s; // 2 //a=2, @ 0 INT32 i; // 4 //a=4, @ 4 BYTE b1; // 1 //a=1, @ 8 BYTE b2; // 1 //a=1, @ 9 } DEMO; typedef struct _test { DEMO de; // 12 char c; // 1 } Test; //16 int main() { INT32 i32; BYTE byte; DEMO d; printf("%ld, %ld\n", sizeof(INT32), sizeof(i32)); // 4, 4 printf("%ld, %ld\n", sizeof(BYTE), sizeof(byte)); // 1, 1 printf("%ld, %ld\n", sizeof(DEMO), sizeof(d)); // 12, 12 printf("%ld\n", sizeof(Test)); // 16 return 0; }
变量属性
C语言中的变量可以有自己的属性, 在定义变量时,可以添加属性关键字,指示变量的独特意义;
变量属性关键字
auto
是C语言中局部变量的默认属性;
- 作用是将局部变量分配到栈区;
- 默认情况下,所有编译器的局部变量都是auto,在栈中指出变量分布;
static
关键字指示变量的静态属性;
static
同时,关键有作用域限定符的意义,static
修改后的局部变量存储在程序静态区域;static
另一个意义是文件作用域标记符;static
修饰的全局变量作用域只是声明的文件中,其他文件不可以访问;static
修改后的函数作用域只在声明文件中,其他文件不能调用;
static
装饰全局变量是一个有限的作用域,装饰局部变量存储在静态区域;static
其意义不是将变量放在静态区域,而是局限于当前文件;
register
关键字指示将变量存储在寄存器中;
register
只要求寄存器变量,但不一定要求成功;register
变量必须是CPU寄存器可接受的值;- 不能运用
&
获取register
由于变量不在内存中; register
不能修改全局变量;- 从寄存器中获得变量会更快,
register
常用于修改循环变量;
小结
auto
变量存储在程序栈中,默认属性;static
局部变量存储在程序静态区中,static全局变量和函数限制文件;register
存储在变量请求中CPU寄存器不一定成功;
///test2.c// static int test2_g = 1; /* * static int test2_g = 1; * 变量test2_g不能引用其他文件 * static变量不在静态区域,但仅限于当前文件 */ int test2_func(){ return test2_g; }
///test.c/// #include <stdio.h> int g = 0; int m = 0; //auto int g = 0; // 全局变量存储在全局区,不能随时分配到可变栈区; // register int m = 0; // 程序运行期间存在全局变量; // 若全局变量允许存储在寄存器中,则多个寄存器变量一直被占用,CPU寄存器将无法正常工作; //extern int test2_g; /* static2.c */ //test2_g变量在static2.c中声明为static,所以这里不能引用 extern int test2_func(); /* static2.c */ void f1() { int i = 0; // 函数调用结束时释放局部变量 i ; printf("%d\t", i); } void f2() { static int i = 0; // static修改变量只初始化一次; // static在修改局部变量时,将局部变量存储在静态存储区而不是栈区,函数运行结束时不会释放; // static局部变量作用域未改变,生命周期延长; i ; printf("%d\t", i); } int main() { auto int i = 0; /* 在程序栈中明确表示I变量存储在 */ register int j = 0; /* 请求存储于CPU中,编译说我会尽力的 */ static int k = 0; /* 告诉编译器不要使用默认属性分配k,但存储在静态区域 */ for (i = 0; i < 5; i ) { f1(); } //打印5个1 printf("\n"); for (i = 0; i < 5; i ) { f2(); } //打印12345 printf("\n"); // printf("test2_g = %d\n", test2_g); printf("test2_g = %d\n", test2_func()); return 0; }
/test.sh #! /bin/bash gcc -o test test.c test2.c
分支语句
if分支
if
语句用于根据条件选择执行语句;else
不能独立存在,最近总是与之不匹配if相匹配;else
句子后可以连接其他if语;
if (condition) {
//statement1;
} else {
//statement2;
}
if (cond1) {
//statement1;
} else if (cond2) {
//statement2;
} else {
//statement3
}
就是以下形式
if (cond1) {
//statement1;
} else {
if (cond2) {
//statement2;
} else {
//statement3
}
}
if语句中0值比较的注意点
bool
型变量应该直接出现于条件中,不需要再比较真假,非0即为真;
bool b = TRUE;
if (b) {
//statement1;
} else {
//statement2;
}
- 普通变量和0值比较时,0值最好出现在比较符号左边;
int i = 1;
if (0 == i) {
//statement1;
} else {
//statement2;
}
float
型变量不能直接进行0值比较,需要定义精度;
#define EPSINON 0.00000001
float f = 0.0;
if ((-EPSINON <= f) && (f <= +EPSINON)) {
//statement1;
} else {
//statement2;
}
switch分支
- switch语句对应单个条件多个分值的情形;
- 每个case语句分支要有break,否则会导致分支重叠;
- default语句必须要加上,以处理特殊情况;
switch (表达式) {
case 常量:
代码块
case 常量:
代码块
default:
代码块
}
- switch语句中的值只能是整型或字符型;
case语句排序顺序分析
- 按字母或数字顺序排列各语句;
- 正常情况放前面,异常情况放后面;
- default语句只能用于真正的默认情况;
if
和switch
对比使用实例
#include <stdio.h>
void f1(int i) {
if (i < 60) {
printf("Failed!\n");
} else if ((60 <= i) && (i <= 80)) {
printf("Good!\n");
} else {
printf("Perfect!\n");
}
}
void f2(char i) {
switch (i) {
case 'c':
printf("Compile\n");
break;
case 'd':
printf("Debug\n");
break;
case 'o':
printf("Object\n");
break;
case 'r':
printf("Run\n");
break;
default:
printf("Unknown\n");
break;
}
}
int main() {
f1(50);
f1(90);
f2('o');
f2('d');
f2('e');
return 0;
}
小结
- if语句适用于需要按片进行判断的情形;
- switch语句适用于需要对各个离散值进行分别判断的情形;
- if语句可以完全从功能上代替switch语句,但switch语句无法代替if语句;
- switch语句对于多分支判断的情形更加简洁;
循环语句
循环语句的基本工作方式: 通过条件表达式(条件表达式遵循if语句表达式的原则)判定是否执行循环体;
do,while,for的区别
- do语句先执行后判断,循环体至少会被执行一次;
- while语句先判断后执行,循环体可能不被执行;
- for语序先判断后执行,相比while更简洁;
三种循环语句使用对比:
//累加自然数
#include <stdio.h>
int f1(int n){
int ret = 0;
int i = 0;
// 最直观
for(i = 1; i <= n; i++) {
ret += i;
}
return ret;
}
int f2(int n){
int ret = 0;
// 实现方式比较抽象
while((n > 0) && (ret += n--)); // 如果没有n>0的判断就会进入死循环
// while(n && (ret += n--)); //死循环
return ret;
}
int f3(int n){
int ret = 0;
if (n > 0) {
// 如果没有n>0的判断就会进入死循环
do {
ret += n--;
} while(n);
// 只要n!=0就是真,while就会继续循环
}
// 可以将条件该为while(n > 0);避免死循环;
// 用while(n>0) do{} 可以代替上面的分支和循环
return ret;
}
int main(){
printf("%d\n", f1(10));
printf("%d\n", f2(10));
printf("%d\n", f3(10));
return 0;
}
break和continue的区别
- break表示终止循环的执行(跳出块语句(循环,switch)),可以用在循环和switch;
- continue表示终止本次循环,进入下次循环执行,仅用于循环体;
switch能否使用continue关键字?
continue是依赖于循环的,不能用于switch分支;
do…while(0)和break的妙用
#include <stdio.h>
#include <malloc.h>
int func(int n)
{
// 1.统一的资源分配
int i = 0;
int ret = 0;
int *p = (int*)malloc(sizeof(int) * n);
// 2.代码执行
do {
if (NULL == p) break; // break跳出语句块
if (n < 0) break; // 如果将break替换为return,代码将比较繁琐
for (i = 0; i < n; i++) {
p[i] = i;
printf("%d\n", p[i]);
}
ret = 1;
} while(0);
// 外层的do...while语句块一定会执行一次
// 3.统一的资源回收
free(p);
return ret;
}
int main() {
if (0 != func(10)) {
printf("OK\n");
} else {
printf("ERROR\n");
}
return 0;
}
遭人遗弃的goto
高手潜规则:禁用goto,程序质量与goto的出现次数成反比;
一般在内核模块的入口函数才会大量使用goto语句,用来处理异常;
其中语句标签是按标识符规定书写的符号,放在某一语句行的前面,标号后加冒号
:
; 语句标签起标示语句的作用,与goto语句配合使用; C语言中不限制程序中使用标签的次数,但各标签不得重名; goto语句是改变程序流向,转去执行语句标签所标识的语句;
- goto语句通常与分支语句配合使用,可用来实现条件转移,构成循环,跳出循环体等功能;
但是结构化程序中不建议使用goto语句,以免造成程序混乱,使理解和调试程序都产生困难;
示例
#include <stdio.h>
int main(){
int n = 0;
printf("input a string\n");
loop:
if (getchar() != '\n') {
n++;
goto loop;
}
printf("%d\n", n);
return 0;
}
例如输入: hello world 回车打印: 11
goto副作用分析
goto有可能会造成跳过一些本来应该执行的语句,破坏结构化程序设计顺序执行的规则;
// goto副作用分析
#include <stdio.h>
void func(int n) {
int* p = NULL;
if (n < 0) { //当n>=0时程序就会执行的很好
goto STATUS; //跳过堆内存的分配,使程序崩溃
}
// 破坏结构化程序的顺序执行
p = malloc(sizeof(int) * n);
STATUS:
p[0] = n;
}
int main() {
f(1);
f(-1);
return 0;
}
编译的时候被编译了,但是执行的时候被goto跳过,造成结果的不确定性;
#include <stdio.h>
int x = 5;
int main () {
printf("%p\n", &x);
goto a;
{
// 执行的时候goto会跳过,但仍然会被正常编译
int x = 3; // 又重新申请了一个同名局部变量x
printf("%p\n", &x);
pritnf("int x = 3\n");
a:
// 这里的x都是局部变量
printf("x = %d\n", x);
printf("%p\n", &x);
}
// 以后的x是全局变量
printf("x = %d\n", x);
printf("%p\n", &x);
return 0;
}
void关键字
-
void修饰函数返回值和参数
- 如果函数没有返回值,那么应该将其声明为void类型;
- 如果函数不接收参数,应该声明其参数为void类型;
void修饰函数返回值和参数仅为了表示无;
-
不存在void变量,
void v;
编译报错; -
没有void的标尺;
- c语言中类型名是固定大小内存的别名,但没有定义void究竟是多大内存的别名;
- void不对应任何变量的大小;
-
void类型的指针是存在的;
void指针的意义
c语言规定只有相同类型的指针才可以相互赋值;
void*
指针作为左值用于接收任意类型的指针;void*
指针作为右值赋值给其他指针时需要强制类型转换,才能够赋值给其他类型的指针;
malloc()
返回void*
类型;
int *pI = (int *)malloc(sizeof(int));
char *pC = (char *)malloc(sizeof(char));
void *p = NULL;
int *pni = NULL;
char *pnc = NULL;
p = pI; //ok
pni = p; //oops!
p = pC; //ok
pnc = p; //oops!
void*
指针的使用,实现my_memset()函数;
#include <stdio.h>
void* my_memset(void *p, char c, int size) {
void *ret = p;
char *dest = (char *)p;
int i = 0;
for (i = 0; i < size; i++) {
dest[i] = c;
}
return ret;
}
int main(){
int arr[5] = {1, 2, 3, 4, 5};
long num = 9999;
char str[10] = "hello";
int i = 0;
for (i = 0; i < 5; i++) {
printf("%d\t", arr[i]);
}
printf("%ld\t", num);
printf("%s", str);
printf("\n");
my_memset(arr, 0, sizeof(arr));
my_memset(&num, 0, sizeof(num));
my_memset(str, 65, sizeof(str) - 1);
for (i = 0; i < 5; i++) {
printf("%d\t", arr[i]);
}
printf("%ld\t", num);
printf("%s", str);
printf("\n");
return 0;
}
extern关键字
- extern用于声明外部定义的变量或函数;
extern "C" {}
用于告诉编译器用c方式编译;
C++编译器和一些变种C编译器默认会按自己的方式编译函数和变量,通过extern关键字可以命令编译器以标准c方式进行编译;
// g++ test.c
#include <stdio.h>
extern "C" {
int add(int a, int b){
return a + b;
}
}
int main(){
printf("res = %d\n", add(2, 3));
return 0;
}
extern用于声明外部定义的变量或函数;
// test2.c
int g = 100;
int get_min(int a, int b) {
return (a < b) ? a : b;
}
//gcc test1.c test2.c
#include <stdio.h>
extern int g; //声明引用外部定义的变量
extern int get_min(int a, int b); // 声明引用外部定义的函数
int main() {
printf("g = %d\n", g);
printf("get_min(3, 5) res %d\n", get_min(3, 5));
return 0;
}
sizeof关键字
sizeof
是编译器的内置指示符,不是函数;sizeof
用于计算相应实体所占的内存大小,不用运行就可以知道,编译时确定;sizeof
的值在编译期就已经确定;sizeof
不是函数;
#include <stdio.h>
int main() {
int a;
printf("%ld\n", sizeof(a));
printf("%ld\n", sizeof a); //sizeof不是函数
printf("%ld\n", sizeof(int));
// printf("%ld\n", sizeof int ); //error: expected expression before ‘int’
C语言中int前面不能出现unsigned/signed/const之外的;
不能是sizeof
类型是不能这么写的;
return 0;
}
const关键字
- const修饰一个只读变量;
- 在c语言中const修饰的变量是只读的,其本质还是变量,在内存中占用空间;
- 本质上const只对编译器有用,在运行时无用;在运行时可以通过一个指针改变其值;
用
const int cc = 1;
定义变量后
- 做左值时,将编译报错;
- 做右值时:
- 直接访问
int cb = cc;
直接从变量表取出内容替换;- 间接访问
int *p = (int *)&cc;
在运行时到内存取值后赋值;
#include <stdio.h>
int main(){
const int cc = 1;
int *p = (int *)&cc;
printf("%d\n", cc);
// cc = 3; //编译器报错
*p = 3; //可以间接改变cc的值
printf("%d\n", cc);
return 0;
}
在c语言中const修饰的数组是只读的;
const修饰的数组空间不可被改变(对现在的c编译器而言)
const int arr[5] = {1, 2, 3, 4, 5}; int *p = (int*)arr; int i = 0; for (i = 0; i < 5; i++) { p[i] = 5 - i; //oops! }
const修饰指针
int const * p;
//p可变,p指向的普通变量的内容不可变;const int * p;
//p可变,p指向的普通变量的内容不可变,与int const *p等价;int * const p;
//p指针不可变,p指向的普通变量的内容可变;int const * const p;
//p和p指向的普通变量的内容都不可变;const int * const p:
//p和p指向的普通变量的内容都不可变,等价于上句;const * int p;
//不合法
const实际上是修饰其左边的东西,const与类型标示符可以更换位置但不能越过
*
号;
口诀
const
相对于*
号,(const在的)左数,(在的右)右指为只读;
- 当const出现在*号左侧时,指针指向的数据为只读;
const char *p = "hello world";
- 当const出现在*号右侧时,指针本身为只读;
const修饰函数参数和返回值
- const修饰的函数参数表示在函数体内不希望被改变;
- const修饰的函数返回值表示返回值不可改变(不能做左值),多用于改变指针的情形;
const int * func() {
static int count = 0;
count++;
return &count;
}
int const *p = func();
//*p = 3;//报错
//p = NULL;//没问题
olatile关键字
- volatile可理解为"编译器警告指示字";
- volatile用于告诉编译器每次必须去内存中取变量的值,不要做优化;
- volatile主要修饰可能被多个线程访问的变量;
- volatile也可修饰可能被未确定因素更改的变量;
int obj = 10;
int a = 0;
int b = 0;
a = obj;
sleep(100);
b = obj;
编译器在编译的时候发现obj没有被当成左值使用,因此会"聪明"的直接将obj替换成10,而把a和b都赋值成10; volatile int obj = 10;
//编译器将每次都直接访问obj的存储位置
const和volatile是否可以同时修饰一个变量?
可以
const volatile int i = 0;
这个时候i具有什么属性,编译器如何处理这个变量?
const告诉我们不应该通过程序来试图修改i的值; volatile告诉编译器i的值随时可能会发生改变,每次引用该变量时都要从内存中读取,以获取最新的结果
驱动程序中常使用这样的方式;
struct空结构体占多大内存
#include <stdio.h>
struct D {
};
int main() {
struct D d1;
struct D d2;
printf("%d\n", sizeof(struct D));
printf("%d, %0x\n", sizeof(d1), &d1);
printf("%d, %0x\n", sizeof(d2), &d2);
return 0;
}
gcc编译器将空结构体大小定义为0,两个不同的变量有着相同的地址(gcc v7.5中为不同地址); g++编译器将空结构体大小定义为1,不会有两个具有相同地址的变量;
由结构体产生柔性数组
柔性数组即数组大小待定的数组; C语言中结构体的最后一个元素可以是大小未知的数组; C语言中可以由结构体产生柔性数组;
struct soft_array{
int len;
int array[];
}
union和struct的区别
- struct中的每个域在内存中都独立分配空间
- union只分配最大域的空间,所有域共享这个空间
struct A{
int a;
int b;
int c;
};
union B{
int a;
int b;
int c;
};
int main(){
printf("%d\n", sizeof(struct A)); //12
printf("%d\n", sizeof(union B)); //4
return 0;
}
union的使用受系统大小端的影响
+----------------------+
| 大端格式 |
| int i = 1; |
| 0x0 0x0 0x0 0x1 |
|----------------------|
| 低地址 高地址 |
+----------------------+
+----------------------+
| 小端格式: |
| 低位数据放在低地址 |
| int i = 1; |
| 0x1 0x0 0x0 0x0 |
|----------------------|
| 低地址 高地址 |
+----------------------+
union U {
int i;
char c;
};
union U u;
u.i = 1;
printf("0x%x\n", u.c); //0x1 or 0x01000000??
如果是小端格式,1会存储在低地址,结果返回1 如果是大端格式,1会存储在高地址,结果返回0
char arr[10] = {1,2,3,4,5,6,7,8,9,10};
//从低地址到高地址依次存放01 02 03 04 05 06 07 08 09 10 ...
int *p = (int *)arr;
printf("0x%x\n", *p); // 0x4030201 //这就是小端格式
int isLittleEndian(void) {
union {
int i;
char c;
} u;
u.i = 1;
printf("union int:1, char:0x%08x\n", u.c);
printf("%s endian\n", (u.c == 1) ? "little" : "big");
return (u.c == 1);
}
Unix和网络的字节序都是高字节序;linux是低字节序;
高字节序又叫大端格式,Big endian:将高序字节存储在起始地址 低字节序又叫小端格式,Little endian:将低序字节存储在起始地址
例子:在内存中整数0x01020304的存储方式
内存地址
&4000 &4001 &4002 &4003
LE 04 03 02 01
BE 01 02 03 04
例子: 如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
BE LE
0x0000 0x12 0xcd
0x0001 0x34 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
x86系列CPU都是little-endian的字节序
enum枚举
enum color{
green, //0
red = 2,
blue //3
};
enum color c = green;
printf("%d\n", c); //0
- enum定义的是真正意义的常量
#define
宏定义的常量只是简单的进行值替换,枚举常量是真正意义上的常量;#define
宏常量无法被调试,枚举常量可以;#define
宏常量无类型信息,枚举常量是一种特定类型的常量`建议尽量使用
enum
而不是使用#define
宏;
typedef
- typedef用于给一个已经存在的数据类型起别名,不具有重新定义的功能;
- typedef并没有产生新的类型;
- typedef重定义的类型不能进行unsigned和signed扩展;
- typedef是给已有类型取别名;
#define
为简单的字符串替换,无别名的概念;
typedef char* PCHAR;
PCHAR p1, p2; //p1,p2都是char类型的指针;
#define PCHAR char*
PCHAR p3, p4; //就是char *p3, p4; p3是指针,p4是普通的char变量
int *p1, *p2; //p1,p2都是指针
int *p1, p2; //p1是指针,p2是普通变量
注释
C语言中的符号 ,.;:?'"()[]{}%^&~-<>!|/#*=+
高手无招胜有招,akari.c,C语言国际混乱大赛最佳展示奖
下面哪些注释是正确的
1 int/*...*/i;
2 char *s="adcdefgh //hijklmn";
3 //Is it a \
valid comment?
4 in/*...*/t i;
/*
,*/
之间的部分将用空格替代;- 出现在双引号之间的注释符号将被忽略,被处理为字符串的一部分
- 续行符可以把后面的一行当作本行的继续
- 编译报错,注释替换后就是
in t i;
注释规则:
- 编译器会在编译的过程删除注释,但不是简单的删除,而是用空格替换
- 编译器认为双引号之间的内容都是字符串,双斜杠也不例外
/*
,*/
型注释不能嵌套;
你觉得y=x/*p
是什么意思
编译器:
将
/*
作为一段注释的开始,把/*
后面的内容当成注释,直到*/
出现为止; 在编译器看来,注释和其他程序元素都是平等的,所以,作为程序员也不能轻看注释.
Note:
写注释不是和人聊天,一定要注意准确有用,避免晦涩和臃肿.
注释原则
- 注释应该准确易懂,防止二义性,错误的注释有害而无利
- 注释是对代码的提示,避免臃肿和喧宾夺主
- 一目了然的代码避免加注释
- 不要用缩写来注释代码,这样可能会产生误解
- 注释用于阐述原因而不是用于描述程序的运行过程
续行符
C语言中的续行符\
是指示编译器行为的利器
#include/*hello world */<stdio.h>
#def\
ine MAX \
255
//#define MAX 255
int main()
{
//\
这是\
\
注释
i\
n\
t\
*\
p\
= \
NULL;
//int * p= NULL;
printf("%p\n", p);
return 0;
}
续行符的使用
- 编译器会将反斜杠后面的字符自动接到前一行
- 在接续单词时,反斜杠后不能有空格,反斜杠的下一行之前也不能有空格
- 接续符适合在定义宏代码块的时候使用
宏代码块的定义
#include <stdio.h>
#define SWAP(a,b) \
{ \
int temp = a; \
a = b; \
b = temp; \
}
转义字符
C语言中的转义字符\
主要用于表示无回显字符,也可以表示常规字符 \n
, \t
, \v
, \b
, \r
, \f
, \\
, \'
, \a
, \ddd
, \xhh
- C语言中的反斜杠同时具有接续符和转义符的作用
- 当反斜杠作为接续符使用时可直接出现在程序中
- 当反斜杠作为转义符使用时续出现在字符活字符串中
单引号与双引号
#include <stdio.h>
int main()
{
char *p1 = (char *) 1 ; //p1指向地址为0x1的地方
char *p2 = (char *)'1'; //p2指向'1'所代表的内存地址49
char *p3 = "1"; //p3指向字符串常量"1"
// printf("p1:%s\n", p1); //Segmentation fault
// printf("p2:%s\n", p2); //Segmentation fault
printf("p3:%s\n", p3); //1
// printf('\n'); //fmt='\n', '\n'=10, Segmentation fault
printf("\n"); //ok
return 0;
}
- C语言中的单引号用来表示字符常量
‘a’ 表示字符常量,在内存中占1个字节,‘a’+1表示’a’的ASCII码+1,结果为’b’;
- C语言中的双引号用来表示字符串常量
"a"表示字符串常量,在内存中占2个字节,“a”+1表示指针运算,结果指向"a"结束符’\0’
#include <stdio.h>
int main()
{
char c = " ";
while (c=="\t" || c==" " || c=="\n") {
scanf("%c", &c);
} //一次循环也不会
return 0;
}
将字符串赋值给一个字符变量char c == " ";
将发生什么?
" "
在内存中有一个空格和一个’\0’组成,假设空格的地址为0xaabbccdd,由于字符类型只有一个字节,因此会截断将dd赋值给c;c == 0xdd;
"\t"
," "
,"\n"
在内存中也有个具体的地址0x********,与c比较是不可能相等的;将以上程序中d双引号全部替换为单引号就可以实现作者的本意;
- 本质上单引号括起来的一个字符代表整数;
- 双引号括起来的字符代表一个指针;
- C编译器接受字符串的比较,可意义是错误的(实际是字符串首地址的比较);
- C编译器允许字符串对字符变量的赋值,其意义是可笑的(实际是将字符串首地址截断后赋值给字符变量);
- C编译器不允许将字符变量赋值给字符串,也不允许将字符串直接赋值给字符串,因为字符串实际就是代表这串字符(只读区)的首地址;
清晰基本概念,远离低级错误
逻辑运算符使用
逻辑运算符&&
,||
和!
#include <stdio.h>
int main() {
int i = 0;
int j = 0;
if (++i > 0 || ++j > 0) {
printf("%d\n", i); //1
printf("%d\n", j); //0
}
return 0;
}
#include <stdio.h>
int main() {
int i = 0;
int j = 0;
if (++i > 0 && ++j > 0) {
printf("%d\n", i); //1
printf("%d\n", j); //1
}
return 0;
}
逻辑运算符的短路特性
||
,&&
从左向右开始计算,当前一个表达式的结果能决定整个表达式的结果,则后面的表达式根本就不会计算或调用;
#include <stdio.h>
int g = 0; //全局变量
int f() {
return g++; //先用后加
}
int main() {
if (f() && f()) { //程序短路,第一个f()被调用并得到0,然后g++;
printf("%d\n", g); //不被执行
}
printf("%d\n", g); //1
return 0;
}
以上代码结果只打印一个1
!
到底是什么
#include <stdio.h>
int main() {
printf("%d\n", !0); //1
printf("%d\n", !1); //0
printf("%d\n", !100); //0
printf("%d\n", !-1000); //0
return 0;
}
C语言中的逻辑符!
只认得0,只知道见了0就返回1;非零的都当作真,作用后都返回0;
三目运算符a?b:c
可以作为逻辑运算符的载体 规则:当a的值为真时,返回b的值,否则返回c的值
#include <stdio.h>
int main() {
int a = 1;
int b = 2;
int c = 0;
// int *p = NULL;
c = a < b ? a : b; //c = 1;
//(a < b ? a : b) = 3; //不能做左值;
*(a < b ? &a : &b) = 3; //合法
// p = (a < b ? &a : &b);
// *p = 3;
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
位运算符
在C语言中的位运算符
&
按位与|
按位或^
按位亦或<<
左移>>
右移~
按位取反(单目运算符)
结合律 a&b&c
<=> (a&b)&c
<=> a&(b&c)
交换律 a&b
<=> b&a
左移和右移注意点
- 左移运算符
<<
将操作数的二进制位左移,高位丢弃,低位补0; - 右移运算符
>>
把操作数的二进制位右移,高位补符号位,低位丢弃;
0x1 << 2 + 3
的值会是什么?32,实际上+
,-
运算的优先级高于移位操作
防错准则:
- 避免位运算符,逻辑运算符和数学运算符同时出现在一个表达式中;
- 当位运算符,逻辑运算符和数学运算符同时参与运算时,尽量使用
()
来表达计算次序;
如何交换两个变量的值
#include <stdio.h>
#define SWAP1(a,b) \
{ \
int temp = a; \
a = b; \
b = temp; \
} //需要使用额外的变量才可以完成
#define SWAP2(a,b) \
{ \
a = a + b; \
b = a - b; \
a = a - b; \
} //a和b很大的时候a+b会溢出
#define SWAP(a,b) \
{ \
a ^= b; \
b ^= b; \
a ^= b; \
}
// a ^= b; => a=(a^b)
// b ^= a; => b=b^(a^b) = a^(b^b) = a^0 => a
// a ^= b; => a=(a^b)^a = (a^a)^b = 0^b => b
// 该方法不用借助其他变量,也不会溢出,而且运算效率高于普通的数学运算
int main() {
int a = 1;
int b = 2;
SWAP1(a,b);
SWAP2(a,b);
SWAP(a,b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
思考1
假设有一数列A,其中的自然数都是出现偶数次,只有一个自然数出现的次数为奇数次,编程找出这个自然数;
方法1
- 排序
- 遍历查找
排序太耗时
方法2
- 遍历,找出最大值max;
- 申请
array[max]
; - 遍历,将
array[A[i]]++
; - 判断哪个下标对应的数值是奇数;
空间复杂度太大
方法3
考虑到亦或^
位操作及交换律,将所有元素亦或操作即可得到该自然数;
#include <stdio.h>
int main() {
int a[] = {2, 3, 3, 5, 7, 2, 2, 2, 5, 7, 1, 1, 9};
int find = 0;
int i = sizeof(a)/sizeof(a[0]);
while (0 <= --i) {
find ^= a[i];
}
printf("find = %d\n", find);
return 0;
}
思考2
&&
,||
,!
与&
,|
,~
的意义是否相同?它们可以在条件表达式中交替使用吗?
不同,一种是逻辑操作,一种是位操作;
1<<32
的结果是什么? 0;1<<-1
的结果又是什么? 0;
自增自减运算符
int i = 3;
(++i) + (++i) + (++i);
你有必要这么写吗? 在C语言里面这是一个灰色地带,C语言规范里面只定义了++操作,但也没有规定这样的表达式如何计算;每一种编译器都有自己的处理方式;
int x = 3;
int k = (++x, x++, x+10);
从左到右顺序求值,然后把最后一个表达式的值作为逗号表达式的结果; 前++是先计算再用,后++是先用,表达式结束的时候再自增; 因此结果是k==14;
笔试面试中的++i+++i+++i
不合法
贪心法:
++
,--
表达式的阅读技巧;
- 编译器处理的每个符号应该尽可能多的包含字符;
- 编译器以从左到右的顺序一个一个尽可能多的读入字符;
- 当即将读入的字符不可能和已读入的字符组合成合法符号为止;
编译器就是贪心; 空格可以结束编译器的贪心;
#include <stdio.h>
int main() {
int i = 0;
int j = ++i+++i+++i;
// 按照贪心法
// ++i+++i+++i
// ++i++ => 2++ => ERROR;
// ++i做左值,不可以再后++自增;
int a = 1;
int b = 2;
int c = a+++b; // a++ +b
int *p = &a;
b = b/*p; // 当编译器读到/时会猜用户可能想做除,然后继续读;
// 读到下一个是*,就把后面的都当成注释了;
// b = b / *p; //是合法的,空格可以结束编译器的贪心;
// 写代码的时候可以适当的使用小括号和空格;可以使代码更美观,也可以适当的防错;
return 0;
}
运算符优先级
- 初等运算符:
() [] -> .
- 单目运算符:
! ~(位取反) ++ -- * & (类型) sizeof
; - 算术运算符:先乘除取余,后加减,再移位;
- 关系运算符:先大小,再判等(
== !=
) - 位操作符:
& ^ |
- 逻辑运算符:
&& ||
- 三目运算符:
?:
- 赋值运算;
- 逗号运算;
#include <stdio.h>
#include <malloc.h>
typedef struct Demo {
int *pInt;
float f;
} Demo;
int func(int v, int m) {
return ((v & m) != 0);
}
int main() {
Demo *pD = (Demo*)malloc(sizeof(Demo));
int *p[5]; //int* p[5]
int i = 0;
i = 1, 2; //i == 1
printf("i:%d\n", i); //i:1
i = (1, 2);//i == 2
printf("i:%d\n", i); //i:2
//*pD.f = 0; //error, 取成员运算符优先级较高
(*pD).f = 0;
free(pD);
return 0;
}
易错的优先级
*p.num
;实际是*(p.num)
.
的优先级高于*
,实际上是对p取偏移,作为指针,然后进行取成员操作; 我们常在程序中用到(*p).num
(等价于p->num
);->
操作符可以消除这个问题;
int *ap[];
实际是int* (ap[]);
[]
的优先级高于*
,实际上ap是个元素为int*
指针的数组, 我们有时候会用到int (*ap)[]
,是一个数组指针;ap指向一个整型数组int a[]
;
int *fp();
实际是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);
,将2的结果给i,1,2
表示两个表达式;
注意
()
,[]
的优先级最高,然后是.
和->
取成员运算符,然后是后++
后--
;再是其他; 注意后++
,后--
结合的优先级很高,但是要等整个语句运行结束时才会使变量的值生效;
*p1++ = *p2++
即*(p1++) = *(p2++)
;
++
先与p结合,然后再与*
结合;while ((*p1++ = *p2++) != '\0');
等价于
do {
*p1 = *p2;
p1++;
p2++;
} while (*(p1-1) != '\0');
类型转换
C语言隐式类型转换
- 算术运算表达式中,低类型转换为高类型;
- 赋值表达式中,表达式的值转换为左边变量的类型;
- 函数调用时,实参转换为形参的类型;
- 函数返回值,return表达式转换为返回值类型;
char -> unsigned char ->
short -> unsigned short ->
int -> unsigned int ->
long -> unsigned long -> double
float -> double
#include <stdio.h>
int main() {
char c = -2;
unsigned char uc = 1;
printf("c+uc=%hhX\n", c + uc);//FF
printf("c+uc=%hX\n", c + uc); //FFFF
printf("c+uc=%X\n", c + uc); //FFFFFFFF
short s = -2;
unsigned short us = 1;
printf("s+us=%hX\n", s + us); //FFFF
printf("s+us=%X\n", s + us); //FFFFFFFF
int i = -2;
unsigned int j = 1;
if ((i + j) >= 0) {
printf("i+j >= 0\n"); //v
} else {
printf("i+j < 0\n");
}
printf("i+j=%X\n", i + j); // FFFFFFFF
printf("i+j=%d\n", i + j); // -1
printf("i+j=%u\n", i + j); // 4294967295
// 有符号和无符号类型在内存中的表示都是一样的;
// 关键看我们的计算机如何解析;
return 0;
}
-2
+2: 00000000 00000000 00000000 00000010
-2: 11111111 11111111 11111111 11111101 + 1
-2: 11111111 11111111 11111111 11111110
-2: 0xFFFFFFFE
+1: 00000000 00000000 00000000 00000001
-2+1:
11111111 11111111 11111111 11111111
0xFFFFFFFF
-1
+1 00000000 00000000 00000000 00000001
-1: 11111111 11111111 11111111 11111110 + 1
-1: 11111111 11111111 11111111 11111111
-1: 0xFFFFFFFF
printf("%d", i + j);
,%d
: 以int类型打印0xFFFFFFFF,被解析为-1 printf("%u", i + j);
,%u
: 以int类型打印0xFFFFFFFF,一个很大的正数;
强制类型转换的实现方式是临时生成一个新数据,使用旧数据对新数据进行赋值; 类型转换并不会改变原数据;
int a = 1;
(unsigned char)a == 1;
(short)a == 1;
(int)a == 1;
并不会因为强制类型转换而多读取a所在的存储空间, 只是用原来的数据临时生成了一个新类型的数据供以后解读;
对指针的强制类型转换会影响程序对数据的读取和解析方式;
#include <stdio.h>
int main() {
char arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
void *p = NULL;
p = arr;
/* 对指针的强制类型转换会影响程序对数据的读取和解析方式 */
int inta = *(int *)p;
short shorta = *(short *)p;
char chara = *(char *)p;
// 被强制类型转换的数据并没有发生变化
printf("%p, %p, %p\n", (int *)p, (short *)p, (char *)p);
// 0x7fff52f3b040, 0x7fff52f3b040, 0x7fff52f3b040
//指针的类型变化,对其解析的方式就会不同,读取对应地址的方式也不同
printf("%x, %x, %x\n", inta, shorta, chara);
// 4030201, 201, 1
return 0;
}
#include <stdio.h>
int main() {
char arr[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
void *p = NULL;
p = arr;
/* 对指针的强制类型转换会影响程序对数据的读取和解析方式 */
printf("%x, %x, %x\n", *(char *)p, *(short *)p, *(int *)p);
/* 1, 201, 4030201 */
/* 强制类型转换的实现方式是临时生成一个新数据, */
/* 使用旧数据对新书据进行赋值;以后的操作采用的都是新数据; */
printf("%x, %x, %x\n", (char)arr[0], (short)arr[0], (int)arr[0]);
/* 1, 1, 1 */
return 0;
}
编译预处理
被编译器隐藏的过程:
file.c + file.h
经过预处理器cpp(删除注释,展开宏等)得到file.i
;- 编译器gcc编译
file.i
得到file.S
(汇编代码); - 汇编器as汇编
file.S
得到目标文件file.o
; - 连接器linker连接目标文件(连接
libc.a
,libc.so
等)得到可执行文件file.out
;
预编译
- 处理所有的注释,以空格代替;
- 将所有的
#define
删除,并且展开替换所有的宏定义; - 处理条件编译指令
#if
,#ifdef
,#elif
,#else
,#endif
; - 处理
#include
,展开被包含的文件; - 保留编译器需要使用的
#pragma
指令;
预处理指令:
gcc -E file.c -o hello.i
编译:
- 对预处理文件进行一系列的词法分析,语法分析和语义分析;
- 词法分析主要分析关键字,标示符,立即数等是否合法;
- 语法分析主要分析表达式是否遵循语法规则;
- 语义分析在语法分析的基础上进一步分析表达式是否合法;
- 分析结束后进行代码优化生成相应的汇编代码文件;
编译指令:
gcc -S file.c -o hello.S
汇编:
汇编器将汇编代码转变为机器可以执行的指令,
每一条汇编语句几乎都对应一条机器指令;
汇编指令:
gcc -C file.s -o hello.o
链接器
链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接;
静态链接
file1.o file2.o libc.a
--链接器linker-- 得到a.out
;
a.out
文件包含file1.o
,file2.o
,libc.a
中所有的文件;缺点是目标文件较大, 优点是运行较快,可独立运行;
动态链接
file1.o lib1.so lib2.so
--链接器linker-- 得到a.out
;
a.out中
不包含lib1.so
和lib2.so
文件的内容,只是在加载的时候链接到so文件;优点是so文件可单独维护,编译目标文件较小; 缺点是运行需要加载,运行速度略慢;
- 编译器将编译工作主要分为预处理,编译和汇编三部分;
- 链接器的工作是把各个独立的模块连接为可执行程序;
- 静态链接在编译期完成,动态链接在运行期完成;
宏
定义宏常量
#define
定义宏常量可以出现在代码的任何地方;#define
从本行开始,之后的代码都可以使用这个宏常量;
#define ERROR -1
#define PI 3.14
#define PATH_0 "D:\cpp\c.ppt"
#define PATH_1 D:\cpp\c.ppt
#define PATH_3 D:\cpp\
c.ppt
以上宏定义都是没有语法错误的; PATH_3
等价于D:\cpp\c.ppt
定义宏表达式
宏也可以给一个计算公式起名字; 宏可以使用参数表示计算公式中未知的内容,参数的个数没有限制; 宏的参数可以代表任何东西,所以宏的参数是没有类型的; 带参数的宏是采用二次替换的方式处理的; 用来给计算公式起名的宏中不能定义自己的变量;
#define
表达式给函数调用的假象,却不是函数#define
表达式可以比函数更强大#define
表达式比函数更容易出错
#define SUM(a, b) (a)+(b)
#define sum(a, b) ((a)+(b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define DIM(array) {sizeof(array) / sizeof(*array)}
//宏只是单纯的展开替换
printf("%d\n", SUM(1,2) * SUM(1,3)); //1 + 2 * 1 + 3 ==5
printf("%d\n", sum(1,2) * sum(1,3)); //(1 + 2) * (1 + 3) == 12
int i = 1, j = 5;
printf("%d\n", MIN(i++, j)); //返回1,而不是2
int i = 1, j = 5;
printf("%d\n", MIN(++i, j)); //返回3,而不是2
int a[] = {1, 2, 3};
printf("%d\n", DIM(a));
前面三个宏都可以找到替代的函数,最后一个宏是找不到对应的函数的;
/*
* 宏macro演示
*/
#include <stdio.h>
//定义一个宏,即给3.14起一个名字PI
#define PI (3.14f)
#define CIRCLE(r) (2 * PI * (r))
#define AREAR(r) (PI * (r) * (r))
int main() {
int radius = 0;
printf("请输入半径: ");
scanf("%d", &radius);
printf("周长是:%g\n", 2 * PI * radius);
printf("周长是:%g\n", CIRCLE(radius));
printf("面积是:%g\n", AREAR(radius));
return 0;
}
宏就是简单的文本展开替换
- 所有用来代表计算公式的宏都应该在计算公式外边加一对小括号,这样可以保证宏代替部分的优先级;
- 所有代表数字的宏参数都应该用小括号包括起来,这样可以防止展开替换后与预期的优先级不一致;
/*
* macro display
*/
#include <stdio.h>
#define SUB1(x, y) (x)- (y)
#define SUB2(x, y) ((x)- (y))
int main() {
printf("SUB1(8, 3) is %d\n", SUB1(8, 3)); //5
printf("21 - SUB1(5, 3) is %d\n", 21 - SUB1(5, 2)); //14,宏展开后违背了预期的优先级导致错误的结果
printf("21 - SUB2(5, 3) is %d\n", 21 - SUB2(5, 2)); //18
printf("SUB2(10, 5-2) is %d\n", SUB2(10, 5 - 2)); //7
return 0;
}
/*
* macro
*/
#include <stdio.h>
#define MUL(x,y) ((x) * (y))
int main() {
printf("MUL(8-2, 9+1) = %d\n", MUL(8 - 2, 9 + 1)); //60
printf("60 / MUL(8-2, 9+1) = %d\n", 60 / MUL(8 - 2, 9 + 1)); //1
return 0;
}
宏只是在编译的预处理阶段进行简单的替换;
如果一个宏里面需要经过复杂的处理才能得到一个结果数字,则这个宏必须写成一个表达式; 不要使用自增或自减的计算结果作为宏的参数;
/*
* macro display
*/
#include <stdio.h>
#define SQUARE(n) ((n) * (n))
int main() {
int num = 4;
printf("SQUARE(num+1) = %d\n", SQUARE(num + 1)); //25
num = 4;
printf("SQUARE(++num) = %d\n", SQUARE(++num)); //36?
num = 4;
printf("SQUARE(num++) = %d\n", SQUARE(num++)); //16?//20
return 0;
}
宏表达式与函数的对比
- 宏表达式在预编译期被处理,编译器不知道宏表达式的存在;
- 宏表达式用"实参"完全替代形参,不进行任何运算;
- 宏表达式没有任何的"调用"开销
- 宏表达式中不能出现递归定义
内置宏
__FILE__
被编译的文件名__LINE__
当前行号__DATE__
编译时的日期__TIME__
编译时的时间__STDC__
编译器是否遵循标准C语言规范
例子
定义日志宏
#include <stdio.h>
#include <time.h>
// #defien LOG(s) printf("%s:%d %s ...\n", __FILE__, __LINE__, s)
#define LOG(s) do { \
time_t t; \
time(&t); \
struct tm *my_tm = localtime(&t);\
printf("%s[%s:%d] %s\n", asctime(my_tm), __FILE__, __LINE__, s); \
} while(0)
void f() {
printf("Enter f() ...\n");
printf("End f() ...\n");
}
int main() {
LOG("Enter main() ...");
f();
LOG("Exit main() ...");
return 0;
}
#define f (x) ((x) - 1)
上面的宏代表什么意思?
编译器认为这是定义宏
f
代表(x) ((x) - 1)
,实际编译时会报错; 如果是相乘需要加*
号((x)*((x) - 1))
; 如果是要定义宏函数应该写成#define f(x) ((x)-1)
;
宏定义对空格敏感吗?
宏定义会把
#define
之后的第一个字段(遇到第一个空格结束)作为宏,后面部分作为宏实体; 所以宏和实体之间是以他们之间的第一个空格分割的,从这个角度看宏定义对空格是敏感的,但后面的实体部分是可以包含空格的,即对空格不敏感;
宏"调用"对空格敏感吗?
宏只是(用实体部分)展开替换,实体部分对空格不敏感,宏调用处的空格也不敏感;# 条件编译 条件编译可以在编译时只编译某些语句而忽略另外一些语句;
- 条件编译是预编译指示命令,用于控制是否编译某段代码;
- 条件编译时选择性编译,而
if...else...
是选择性执行;
条件编译
条件编译可以在编译时只编译某些语句而忽略另外一些语句;
条件编译形式1
#ifdef 宏名称
...
#else
...
#endif
#ifndef 宏名称