C语言深度剖析

//
01. basic_data_type
//
关键字的秘密

数据类型
什么是数据类型
数据类型可以理解为固定内存大小的别名;
数据类型是创建变量的,模子->花形的圆形的星形的等等;
char 1byte
short 2byte
int 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("%d, %d\n", sizeof(char), sizeof(c)); /* 1, 1 */ 
 /* 变量的大小与定义他的类型大小是一样的 */
 /* 深刻理解数据类型与变量的关系 */
    printf("%d, %d\n", sizeof(short), sizeof(s));  /* 2, 2 */ 
    printf("%d, %d\n", sizeof(int), sizeof(i)); /* 4, 4 */ 
    
    return 0;
}


打开思维导图
1.自定义类型+>typedef
2.创建变量+>type name
3.通过打印语句证明本质+>printf();

#include <stdio.h>
typedef int INT32;
typedef unsigned char BYTE;
typedef struct _demo {
 short s; /* 2 */
 INT32 i; /* 4 */
 BYTE b1; /* 1 */
 BYTE b2; /* 1 */
} DEMO;
typedef struct test {
 DEMO de; /* 12 */
 char c;  /* 1 */
} test;

int main() {
 INT32 i32;
 BYTE byte;
 DEMO d;
 /* 4, 4 */
 printf("%d, %d\n", sizeof(INT32), sizeof(i32));
 /* 1, 1 */
 printf("%d, %d\n", sizeof(BYTE), sizeof(byte));
 /* 12, 12 */
 printf("%d, %d\n", sizeof(DEMO), sizeof(d));
 /* 16 */
 printf("%d\n", sizeof(test));
 return 0;
}

//
02. auto.register.static
//
C语言中变量可以有自己的属性,
在定义变量的时候可以加上属性关键字;
属性关键字指明变量的特有意义;

auto是c语言中局部变量的默认属性
 作用就是让一个局部变量分配到栈区;
 编译器默认所有的局部变量都是auto
 指明变量分配在栈

static关键字指明变量的静态属性
 static关键字同时具有作用域限定符的意义;
 static修饰的局部变量存储在程序静态区;

 static的另外一个意义是文件作用域标示符;
 static修饰的全局变量作用域只是声明的文件中;
 static修饰的函数作用域只是声明的文件中;其他文件不可以调用;
static修饰全局变量是限定作用域,修饰局部变量是存储在静态区;

register关键字指明将变量存储在寄存器中
 register只是请求寄存器变量,但不一定请求成功;
 register变量必须是CPU寄存器可以接受的值;
 不能运用&运算获取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
//
03. if.switch.do_while.for
//
分支语句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值比较的注意点
 1.bool型变量应该直接出现于条件中,不需要再比较真假,非0即为真;
 2.普通变量和0值比较时,0值最好出现在比较符号左边;
 3.float型变量不能直接进行0值比较,需要定义精度;

bool b = TRUE;
if (b) {
 //statement1;
} else {
 //statement2;
}

int i = 1;
if (0 == i) {
 //statement1;
} else {
 //statement2;
}

#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 < 6 ) {
        printf("Failed!\n");
    } else if( (6 <= i) && (i <= 8) ) {
        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(5);
    f1(9);
    
    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和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( func(10) ) {
        printf("OK");
    } else {
        printf("ERROR");
    }

 return 0;
}
-----------------------------------------------

//
04. goto.void.extern.sizeof
//
遭人遗弃的goto
高手潜规则:禁用goto,程序质量与goto的出现次数成反比;
一般在内核模块的入口函数才会大量使用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编译器默认会按自己的方式编译函数和变量,通过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("%d\n", sizeof(a));
    printf("%d\n", sizeof a); //sizeof不是函数
    printf("%d\n", sizeof(int));
 /* printf("%d\n", sizeof int );  */
 C语言中int前面不能出现unsigned/signed/const之外的;
 不能是sizeof
 类型是不能这么写的;
    
    return 0;
}



//
05. const.volatile
//
const关键字
const修饰一个只读变量
在c语言中const修饰的变量是只读的,其本质还是变量,在内存中占用空间
本质上const只对编译器有用,在运行时无用;在运行时可以通过一个指针改变其值;

用const int cc = 1;定义变量后
做左值时,将报错
做右值时,
  1.直接访问 int cb = cc;直接从变量表取出内容替换;
  2.间接访问 int *p = (int *)&cc; 在运行时到内存取值后赋值;

在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出现在*号右侧时,指针本身为只读

const修饰函数参数和返回值
const修饰的函数参数表示在函数体内不希望被改变参数
const修饰的函数返回值表示返回值不可改变(不能做左值),多用于改变指针的情形;

const int * func() {
 static int count = 0;
 count++;
 return &count;
}
int const *p = func();


#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;
}


volatile关键字
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的值随时可能会发生改变,每次引用该变量时都要从内存中读取,以获取最新的结果


//
06. struct.union
//
struct
空结构体占多大内存
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 //两个不同的变量有着相同的地址
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;         |
| 0x1  0x0  0x0  0x0   |
|----------------------|
| 高地址      低地址   |
+----------------------+
+----------------------+
|   小端模式           |
|   int i = 1;         |
| 0x0  0x0  0x0  0x1   |
|----------------------|
| 高地址      低地址   |
+----------------------+
union C{
 int i;
 char c;
};
union C c;
c.i = 1;
printf("%d\n", c.c); //??




//
07. enum.typedef
//
enum是一种自定义类型
enum默认常量在前一个直的基础上依次+1
enum类型的变量只能取定义时的离散值
enum color{
 green, //0
 red = 2;
 blue //3
};
enum color c = green;
printf("%d\n", c);

enum定义的是真正意义的常量
#define宏定义的常量只是简单的进行值替换,枚举常量是真正意义上的常量
#define宏常量无法被调试,枚举常量可以
#define宏常量无类型信息,枚举常量是一种特定类型的常量
建议尽量使用enum而不是使用#define宏

typedef用于给一个已经存在的数据类型起别名,不具有重新定义的功能;
typedef并没有产生新的类型
typedef重定义的类型不能进行unsigned和signed扩展
typedef是给已有类型取别名
#define为简单的字符串替换,无别名的概念

typedef char* PCHAR;
PCHAR p1, p2;
//p1,p2都是char类型的指针;
#define PCHAR char*
PCHAR p3, p4;
//p3是指针,p4是普通的char变量
int *p1, *p2;
int *p1, p2;

//
08. annotate
//
C语言中的符号
,.;:?'"()[]{}%^&~-<>!|/#*=+
高手五招胜有招,akari.c,C语言国际混乱大赛最佳展示奖

下面哪些注释是正确的
 1 int/*...*/i;
 2 char *s="adcdefgh    //hijklmn";
 3 //Is it a \
    valid comment?
 4 in/*...*/t i;

 1 /**/之间的部分将用空格替代
 2 出现在双引号之间的注释符号将被忽略,被处理为字符串的一部分
 3 续行符可以把后面的一行当作本行的继续

注释规则:
 编译器会在编译的过程删除注释,但不是简单的删除,而是用空格替换
 编译器认为双引号之间的内容都是字符串,双斜杠也不例外
 /**/型注释不能被嵌套

你觉得y=x/*p是什么意思
编译器:
 将/*作为一段注释的开始,把/*后面的内容当成注释,直到*/出现为止
 在编译器看来,注释和其他程序元素都是平等的,所以,作为程序员也不能轻看注释.

Note:
 写注释不是和人聊天,一定要注意准确有用,避免晦涩和臃肿.

注释应该准确易懂,防止二义性,错误的注释有害而无利
注释是对代码的提示,避免臃肿和喧宾夺主
一目了然的代码避免加注释
不要用缩写来注释代码,这样可能会产生误解
注释用于阐述原因而不是用于描述程序的运行过程


//
09. slash_backslash
//
续行符
C语言中的续行符(\)是指示编译器行为的利器
#def\
ine MAX\
255

int main()
{
/\
这是\
\
注释

i\
n\
t\
 *\
 p\
= \
 NULL;

printf("%0X\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语言中的反斜杠同时具有接续符和转义符的作用
当反斜杠作为接续符使用时可直接出现在程序中
当反斜杠作为转义符使用时续出现在字符活字符串中

//
10. quotation
//
单引号与双引号

#include <stdio.h>
int main()
{
    char* p1 =  1 ;
    char* p2 = '1';
    char* p3 = "1";
    printf("%s, %s, %s", p1, p2, p3); //段错误
    printf('\n'); //段错误;
    printf("\n");
    return 0;
}
    
char *p1 =  1 ; //p1指向地址为0x1的地方
char *p2 = '1'; //p2指向'1'所代表的内存地址49
char *p3 = "1"; //p3指向字符串常量"1"

printf('\n');
printf(const char *fmt);
 fmt = '\n'; '\n' == 10

C语言中的单引号用来表示字符常量
C语言中的双引号用来表示字符串常量

'a' 表示字符常量,在内存中占1个字节,'a'+1表示'a'的ASCII码+1,结果为'b'

"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编译器允许字符串对字符变量的赋值,其意义是可笑的;

清晰基本概念,远离低级错误

//
11. logical_operation_symbol
//
逻辑运算符使用

逻辑运算符&&,||和!

#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() ) { //程序短路
        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;
}

//
12. bit_operation
//
位运算符
在C语言中的位运算符
 & 按位与 
 | 按位或
 ^ 按位亦或
 << 左移
 >> 右移
 ~ 按位取反(单目运算符)

结合律 a&b&c <=> (a&b)&c <=> a&(b&c)
交换律 a&b <=> b&a

左移和右移注意点
左移运算符<<将运算数的二进制位左移,高位丢弃,低位补0;
右移运算符>>把运算数的二进制位右移,高位补符号位,低位丢弃;

0x1 << 2 + 3 的值会是什么
实际上+-运算的优先级高于移位操作

防错准则:
避免位运算符/逻辑运算符和数学运算符同时出现在一个表达式中;
当位运算符/逻辑运算符和数学运算符同时参与运算时,尽量使用()来表达计算次序;

如何交换两个变量的值

#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 SWAP3(a,b) \
{                  \
    a ^= b;     \
    b ^= b;     \
    a ^= b;     \
}
/*
 * a ^= b; => (a^b)
 * b ^= a; => b^(a^b) => a^(b^b) => a^0 => a
 * a ^= b; => (a^b)^(a) => a^a^b => 0^b =>b
 * 该方法不用借助其他变量,也不会溢出
 * 而且运算效率高于普通的数学运算
 */

int main() {
    int a = 1;
    int b = 2;
    SWAP1(a,b);
    SWAP2(a,b);
    SWAP3(a,b);
 printf("a = %d, b = %d\n", a, b);
    return 0;
}

练习:
假设有一数列A,其中的自然数都是出现偶数次,只有一个自然数出现的次数为奇数次,编程找出这个自然数;
方法1:
 1.排序
 2.遍历查找
 //缺点,排序太耗时
方法2:
 1.遍历,找出最大值max
 2.申请array[max]
 3.遍历,将array[A[i]]++;
 4.判断哪个下标对应的数值是奇数
方法3;
 考虑到亦或位操作及交换律,将所有元素亦或操作即可得到该自然数

#include <stdio.h>
int main() {
 int a[] = {2, 3, 5, 7, 2, 2, 2, 5, 7, 1, 1, 9};
 int find = 0;
 int i = 0;

 for (i = 0; i < sizeof(a)/sizeof(*a); i++) {
  find ^= a[i];
 }
 printf("find = %d\n", find);
 return 0;
}
思考:
&&,||,!与&,|,~的意义是否相同?它们可以在条件表达式中交替使用码?
 //不同,一种是逻辑操作,一种是位操作;
1<<32的结果是什么?1<<-1的结果又是什么?//0,0

//
13. ++.--
//
++,--

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;
}

//
14. priority_typeconversion
//

#include <stdio.h>
#include <malloc.h>
typedef struct _demo {
    int* pInt;
    float f;
} Demo;
int func(int v, int m) {
    /* return (v & m != 0); //v & (m != 0) */
    return ((v & m) != 0);
}
int main() {   
    Demo* pD = (Demo*)malloc(sizeof(Demo));
    int *p[5]; //int* p[5]
    int *f(); //int* f();
    int i = 0;
    /* i = 1, 2; //i == 1 */
    i = (1, 2);
    /* *pD.f = 0; // *(pD.f) */
    (*pD).f = 0;
    free(pD);
    return 0;
}


易错的优先级
*p.num;
.的优先级高于*,->操作符作用于消除这个问题
 实际上是对p取f偏移,作为指针,然后进行取成员操作 <=> *(p.num)

int *ap[];
[]的优先级高于*
实际上ap是个元素为int*指针的数组 <=> int *(ap[])

int *fp();
函数()高于*
fp是个函数,返回int* <=> int*(fp())

(val & mask != 0)
==和!=高于位操作
<=> val & (mask != 0)

c = getchar() != EOF;
==和!=高于赋值操作
<=> (getchar() != EOF)

msb << 4 + lsb
算术运算符高于位移运算符
<=> msb << (4 + lsb)

i = 1, 2;
逗号运算符在所有运算符中优先级最低
<=> (i = 1), 2;
 

C语言隐式类型转换
算术运算表达式中,低类型转换为高类型;
赋值表达式中,表达式的值转换为左边变量的类型;
函数调用时,实参转换为形参的类型;
函数返回值,return表达式转换为返回值类型;

char ->
short -> int -> unsigned int -> long -> unsigned long -> double <- float


#include <stdio.h>
int main() {
    int i = -2;
    unsigned int j = 1;
    if ( (i + j) >= 0 ) {
        printf("i+j >= 0\n");
    } else {
        printf("i+j < 0\n");
    }
    printf("i+j=%0X\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类型打印i+j; 结果是0xFFFFFFFF,被解析为-1
%u: 把-2转为uint,0xFFFFFFFE很大的正数;

//
15. compilation_process
//
编译预处理

被编译器隐藏的过程:
file.c + file.h ->
 预处理器cpp(删除注释,展开宏等) ->
 file.i ->
 编译器gcc ->
 file.S (汇编代码) ->
 汇编器as ->
 file.o ->
 连接器linker(链接libc.a lib.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文件可单独维护,编译目标文件较小;缺点是运行需要加载,速度略慢;

编译器将编译工作主要分为预处理,编译和汇编三部分;
链接器的工作是把各个独立的模块连接为可执行程序;
静态链接在编译期完成,动态链接在运行期完成;




//
16. macro
//

定义宏常量
#define定义宏常量可以出现在代码的任何地方;
#define从本行开始,之后的代码都可以使用这个宏常量

#define ERROR -1
#define PI 3.14
#define PATH_0 "D:\delphi\c.ppt"

#define PATH_1 D:\delphi\c.ppt
#define PATH_3 D:\delphi\
c.ppt

以上宏定义都是没有语法错误的;
PATH_3等价于 D:\delphic.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));

前面三个宏都可以找到替代的函数,最后一个宏是找不到对应的函数的;

宏强于函数的优势

宏可以用来扩展C语言的关键字和用法
#include <stdio.h>
#include <malloc.h>

#define MALLOC(type, x) (type*)malloc(sizeof(type)*x)
#define FOREVER() while(1)

#define BEGIN {
#define END   }
#define FOREACH(i, m) for(i=0; i<m; i++)

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int x = 0;
    int* p = MALLOC(int, 5);
    
    FOREACH(x, 5)
    BEGIN
        p[x] = array[x];
    END
    
    FOREACH(x, 5)
    BEGIN
        printf("%d\n", p[x]);
    END
    
    FOREVER();
    
    free(p);
    
    printf("Last printf...\n");
    
    return 0;
}

宏表达式与函数的对比
宏表达式在预编译期被处理,编译器不知道宏表达式的存在;
宏表达式用"实参"完全替代形参,不进行任何运算;
宏表达式没有任何的"调用"开销
宏表达式中不能出现递归定义
#define FAC(n) ((n>0) ? (FAC(n-1)+n) : 0)
int j = FAC(10);
通过单步编译,查看预编译的结果可知,替换后的结果为:
int j = FAC(9) + 10;

宏定义的常量或表达式是否有作用域限制?
int f1(int a, int b) {
 #define _MIN_(a, b) ((a) < (b) ? (a) : (b))
 return _MIN_(a, b);
 #undef _MIN_
 // 有了宏,可以定义类似局部函数的概念
}
int f2(int a, int b, int c) {
 return _MIN_(_MIN_(a, b), c);
}
int main() {
 printf("%d\n", f1(2, 1));
 printf("%d\n", f2(5, 3, 2));
 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; \
 struct tm* my_tm; \
 time(&t); \
 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() ...");
}

#define f (x) ((x) - 1)
上面的宏代表什么意思?
宏定义对空格敏感吗?宏"调用"对空格敏感吗?



//
17. conditional_compilation
//
条件编译
条件编译的行为类似于C语言中的if...else
条件编译是预编译指示命令,用于控制是否编译某段代码

/* #define C 1 */
int main() {
 #if (C == 1)
  printf("This is 1st printf ...\n");
 #else
  printf("This is 2nd printf ...\n");
 #endif

 return 0;
}

也可以在预编译的时候指定编译的选项
gcc -DC=1 -E test.c -o test.i


#include 的困惑
#include的本质是将已经存在的文件内容嵌入到当前文件中
#include的间接包含同样会产生嵌入文件内容的动作
间接包含同一个头文件是否会产生编译错误?会
使用条件编译随心所欲的使用头件


// global.h
int global = 10;

// test.h
#include <stdio.h>
#include "global.h"
const char* NAME = "Hello world!";
void f() {
    printf("Hello world!\n");
}

// test.c
#include <stdio.h>
#include "test.h"
#include "global.h"
int main() {
    f();
    printf("%s\n", NAME);
    return 0;
}

编译报错:
glocal.h:1: error: redefinition of 'global'
glocal.h:1: error: previous definition of 'global' was here

使用单步编译,查看预处理后的文件;
可以发现global.h被包含了两次

为头文件添加条件编译宏,防止重复包含:
#ifndef _TEST_H_
#define _TEST_H_
 code ;
#endif

例子:
// global.h
#ifndef _GLOBAL_H_
#define _GLOBAL_H_
int global = 10;
#endif

// test.h
#ifndef _TEST_H_
#define _TEST_H_
#include <stdio.h>
#include "global.h"
const char* NAME = "Hello world!";
void f() {
    printf("Hello world!\n");
}
#endif

// test.c
#include <stdio.h>
#include "test.h"
#include "global.h"

int main() {
    f();
    printf("%s\n", NAME);
    return 0;
}

条件编译的意义
条件编译使得我们可以按不同的条件编译不同的代码段,因而产生不同的目标代码;
#if ... #else ... #endif被预编译器处理;而if...else语句被编译器处理,必然被编译进目标代码;
实际工程中条件编译主要用于以下两种情况:
 不同的产品线共用一份代码;
 区分编译产品的调试版和发布版;

例子:
产品线区分及调试代码应用

#include <stdio.h>

// 区分调试版和发布版
#ifdef DEBUG
    #define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
#else
    #define LOG(s) NULL
#endif

// 区分乞丐版和高级版
#ifdef HIGH
void f() {
    printf("This is the high level product!\n");
}
#else
void f() {
}
#endif

int main() {
    LOG("Enter main() ...");
    
    f();
    
 // 低配 乞丐版
    printf("1. Query Information.\n");
    printf("2. Record Information.\n");
    printf("3. Delete Information.\n");
    
 // 顶配 高级版
    #ifdef HIGH
    printf("4. High Level Query.\n");
    printf("5. Mannul Service.\n");
    printf("6. Exit.\n");
    #else
    printf("4. Exit.\n");
    #endif
    
    LOG("Exit main() ...");
    
    return 0;
}


通过编译器命令行能够定义预处理器使用的宏
条件编译可以避免重复包含同一个头文件
条件编译可以在工程开发中区别不同产品线的代码
条件编译可以定义产品的发布版和调试版


//
18. #error.#line
//
自定义编译错误信息

#error的用法
#error用于生成一个编译错误消息,并停止编译
用法:
 #error message
注意:message不需要用双引号包围
#error编译指示字用于自定义程序员特用的编译错误消息
类似的,#warning用于生成编译警告,但不会停止编译


#include <stdio.h>

#define CONST_NAME1 "CONST_NAME1"
#define CONST_NAME2 "CONST_NAME2"

int main() {  
    #ifndef COMMAND
    #warning Compilation will be stoped ...
    #error No defined Constant Symbol COMMAND
    #endif

    printf("%s\n", COMMAND);
    printf("%s\n", CONST_NAME1);
    printf("%s\n", CONST_NAME2);

    return 0;
}

gcc -DCOMMAND test.c

#line的用法
#line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号;
用法:
 #line number filename
注: filename可以省略
#line编译指示字的本质是重定义__LINE__和__FILE__
用于早期开发的调试

#include <stdio.h>

#define CONST_NAME1 "CONST_NAME1"
#define CONST_NAME2 "CONST_NAME2"
void f();
int main() {
    printf("%s\n", CONST_NAME1);
    printf("%s\n", CONST_NAME2);
    printf("%d\n", __LINE__);
    printf("%s\n", __FILE__);
    f();
    return 0;
}

/* 以下代码有long编写 */
/* 以下代码有long编写 */
/* 以下代码有long编写 */
#line 4 "long.c"
/* #line 4 "long.c" 的下一行被定义为4行,名字被定义为long.c */
void f() {
 return 0;
}



//
19. #pragma
//
#pragma预处理
#pragma是编译器指示字,用于指示编译器完成一些特定的动作
#pragma所定义的很多指示字是编译器和操作系统所独有的
#pragma在不同的编译器间是不可移植的
 预处理器将忽略它不认识的#pragma指令
 两个不同的编译器可能以两种不同的方式解释同一条#pragma指令
一般用法:
 #pragma parameter
注:不同的parameter参数语法和意义各不相同

#pragma message
message参数在大多数的编译器中都有相似的实现
message参数在编译时输出消息到编译输出窗口中
message可用于代码的版本控制
注意:message是VC特有的编译器指示字,GCC中将其忽略;


#include <stdio.h>

/* #define ANDROID23 1 */
#if defined(ANDROID20)
    #pragma message("Compile Android SDK 2.0...")
    #define VERSION "Android 2.0"
#elif defined(ANDROID23)
    #pragma message("Compile Android SDK 2.3...")
    #define VERSION "Android 2.3"
#elif defined(ANDROID40)
    #pragma message("Compile Android SDK 4.0...")
    #define VERSION "Android 4.0"
#else
    #error Compile Version is not provided!
#endif

int main() {
    printf("%s\n", VERSION);
    return 0;
}

#pragma pack
内存对齐
什么是内存对齐?
 不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个挨一个的排放,这就是对齐;
 任何变量的地址必须是P的整数倍.这个规则叫做//数据对齐.
 P取:#pragma pack(n) 指定的字节数(2/4/8/...)和这个变量自身大小二者中的最小值;
 数据对齐会造成结构体内部不同子变量之间有空隙.

 一个结构体变量的大小必须是P的整数倍).这个规则叫//数据补齐.
 P取:#pragma pack(n) 指定的字节数(2/4/8/...)和结构体中最大的子变量二者中的最小值;
 #pragma pack() 默认是4字节
这种补齐可能造成结构体在最后多占用一些浪费的字节.
"结构体中子变量的顺序会影响结构体的大小.
占用空间小的子变量写前边可以节约内存空间.

 struct test1{
  char c1; // 1
  short s; // 2
  char c2; // 1
  int i; // 4
 }
 struct test2{
  char c1; // 1
  char c2; // 1
  short s; // 2
  int i; // 4
 }
test1和test2两种类型所占用的内存空间是否相同?

为什么需要内存对齐?
 CPU对内存的读取是不连续的,而是分成块读取的,块的大小值能是1,2,4,8,16字节;
 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣;
 某些硬件平台只能从规定的地址处取某些特定类型的数据,否则抛出硬件异常;

#pragma pack能够改变编译器的默认对齐方式
#pragma pack(2) //指定以2字节对齐
#pragma pack(4) //指定以4字节对齐
#pragma pack()  //指定以默认字节(4)对齐

#include <stdio.h>
struct test1{
 char c1;
 short s;
 char c2;
 int i;
}; //12
struct test2{
 char c1;
 char c2;
 short s;
 int i;
}; //8

int main() {
 printf("%d, %d\n", sizeof(struct test1), sizeof(struct test2)); //12, 8
 return 0;
}


#include <stdio.h>
#pragma pack(2)
struct test1{
 char c1;
 short s;
 char c2;
 int i;
}; // 10
struct test2{
 char c1;
 char c2;
 short s;
 int i;
}; // 8
#pragma pack()

int main() {
 printf("%d, %d\n", sizeof(struct test1), sizeof(struct test2)); // 10, 8
 return 0;
}

struct占用内存大小
 每一个成员起始与0偏移处;
 每一个成员按其类型大小和指定对齐参数n中较小的一个进行对齐;
  偏移地址和成员占用大小均需对齐;
  结构体成员的对齐参数为其所有成员使用的对齐参数最大值;
 结构体总长度必须为所有对齐参数的整数倍;


#include <stdio.h>

#pragma pack(8)
#pragma pack(4)
/* #pragma pack(2) */
struct S1 {
    short a; // 2
    long b; // 8
};
struct S2 {
    char c; // 1
    struct S1 d; // 2+8
    double e; // 8
};
#pragma pack()

int main() {
 struct S1 s1;
    struct S2 s2;
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    printf("%p, %p\n", &(s1.a), &(s1.b));
    printf("%p, %p, %p\n", &(s2.c), &(s2.d), &(s2.e));
    printf("%p, %p\n", &(s2.d.a), &(s2.d.b));
    return 0;
}


//
20. #_##
//
#运算符用于在预编译期将宏参数转换为字符串

#include <stdio.h>
#define CONVERS(x) #x
int main(){
 printf("%s\n", CONVERS(Hello world!));
 pritnf("%s\n", CONVERS(100));
 printf("%s\n", CONVERS(while));
 printf("%s\n", CONVERS(return));
 return 0;
}

#运算符在宏中的妙用

#include <stdio.h>
#define CALL(f, p) (printf("Call function %s\n", #f), f(p))
// 打印一句call function,然后再调用函数
int square(int n) {
    return n * n;
}
int f(int x) {
    return x;
}

int main() {
    printf("1. %d\n", CALL(square, 4));
    printf("2. %d\n", CALL(f, 10));
    return 0;
}

##运算符用于在预编译期粘连两个符号

#include <stdio.h>
#define STR(n) #n
#define LOCAL(n) woshiqianzui_##n
int main(){
 printf("STR(abc) is %s\n",STR(abc));
 int woshiqianzui_num = 10;
 int LOCAL(num1) = 20;//与上一句等效,可以方便书写
 printf("%d\n",woshiqianzui_num);
 printf("%d\n",LOCAL(num1));
 return 0;
}

利用##定义结构体类型

#include <stdio.h>
#define STRUCT(type) typedef struct _tag_##type type;\
struct _tag_##type

STRUCT(Student) {
    char* name;
    int id;
};

int main() {
    Student s1;
    Student s2;

    s1.name = "s1";
    s1.id = 0;
    s2.name = "s2";
    s2.id = 1;
    printf("%s\n", s1.name);
    printf("%d\n", s1.id);
    printf("%s\n", s2.name);
    printf("%d\n", s2.id);
    return 0;
}



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值