C语言深度剖析笔记1

关键字的秘密

数据类型

什么是数据类型?
数据类型可以理解为固定内存大小的别名;
数据类型是创建变量的模子(花形的,圆形的,星形的等等);

  • 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("%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语言中变量可以有自己的属性,
在定义变量的时候可以加上属性关键字,属性关键字指明变量的特有意义;

变量属性关键字

  1. auto是C语言中局部变量的默认属性;
  • 作用就是让一个局部变量分配到栈区;
  • 编译器默认所有的局部变量都是auto,指明变量分配在栈;
  1. static关键字指明变量的静态属性;
  • static关键字同时具有作用域限定符的意义,static修饰的局部变量存储在程序静态区;
  • static的另外一个意义是文件作用域标示符;
    • static修饰的全局变量作用域只是声明的文件中,其他文件不可以访问;
    • static修饰的函数作用域只是声明的文件中,其他文件不可以调用;

static修饰全局变量是限定作用域,修饰局部变量是存储在静态区;
static的意义不是将变量放在静态区,而是限定在当前文件;

  1. 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值比较的注意点

  1. bool型变量应该直接出现于条件中,不需要再比较真假,非0即为真;
bool b = TRUE;
if (b) {
    //statement1;
} else {
    //statement2;
}
  1. 普通变量和0值比较时,0值最好出现在比较符号左边;
int i = 1;
if (0 == i) {
    //statement1;
} else {
    //statement2;
}
  1. 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语句只能用于真正的默认情况;

ifswitch对比使用实例

#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常常会破坏结构化程序的顺序执行;
  • 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;定义变量后

  • 做左值时,将编译报错;
  • 做右值时:
    1. 直接访问 int cb = cc;直接从变量表取出内容替换;
    2. 间接访问 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是一种自定义类型;
  • enum默认常量在前一个直的基础上依次+1;
  • 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;
  1. /*,*/之间的部分将用空格替代;
  2. 出现在双引号之间的注释符号将被忽略,被处理为字符串的一部分
  3. 续行符可以把后面的一行当作本行的继续
  4. 编译报错,注释替换后就是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

  1. 排序
  2. 遍历查找

排序太耗时

方法2

  1. 遍历,找出最大值max;
  2. 申请array[max];
  3. 遍历,将array[A[i]]++;
  4. 判断哪个下标对应的数值是奇数;

空间复杂度太大

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

运算符优先级

  1. 初等运算符:() [] -> .
  2. 单目运算符:! ~(位取反) ++ -- * & (类型) sizeof;
  3. 算术运算符:先乘除取余,后加减,再移位;
  4. 关系运算符:先大小,再判等(== !=)
  5. 位操作符: & ^ |
  6. 逻辑运算符:&& ||
  7. 三目运算符: ?:
  8. 赋值运算;
  9. 逗号运算;
#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;
}

编译预处理

被编译器隐藏的过程:

  1. file.c + file.h经过预处理器cpp(删除注释,展开宏等)得到file.i;
  2. 编译器gcc编译file.i得到file.S(汇编代码);
  3. 汇编器as汇编file.S得到目标文件file.o;
  4. 连接器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.solib2.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 宏名称 
    ... 
#else 
    ... 
#endif

以上语句可以根据某个宏是否被定义过而从两组语句中选择一组编译;

/*
 * 条件编译
 */
#include <stdio.h>
int main() {
//#ifdef ONE
#ifndef TWO
    printf("1\n");
#else
    printf("2\n");
#endif
    return 0;
}

条件编译形式2

#if    逻辑表达式
    ...
#elif  逻辑表达式(多个)
    ...
#else
    ...
#endif

以上结构可以根据任何逻辑表达式从多组语句中选择一组编译;

#include <stdio.h>
// #define C 1
int main() {
#if (C == 1)
    printf("This is 1st printf ...\n");
#else
    printf("This is 2nd printf ...\n");
#endif
    return 0;
}

也可以在预编译的时候通过-D选项指定编译的选项

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

void f() {
// 区分乞丐版和高级版
#ifdef HIGH
    printf("This is the high level product!\n");
#else
    printf("This is the normal product!\n");
#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;
}

技巧总结

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

自定义编译错误信息

#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 undefined Constant Symbol COMMAND
#endif

    printf("%d\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>
void func();
int main() {
    printf("__func__:%s, %s, %d\n", __func__, __FILE__, __LINE__); //test.c 4
    func();
    printf("__func__:%s, %s, %d\n", __func__, __FILE__, __LINE__); //test.c 6
    return 0;
}

// 以下代码有long编写
// 以下代码有long编写
// 以下代码有long编写
#line 4 "long.c"
// #line 4 "long.c" 的下一行被定义为4行,名字被定义为long.c
void func() {
    printf("__func__:%s, %s, %d\n", __func__, __FILE__, __LINE__);//long.c 6
}

#pragma预处理

  • #pragma是编译器指示字,用于指示编译器完成一些特定的动作;
  • #pragma所定义的很多指示字是编译器和操作系统所独有的;
  • #pragma在不同的编译器间是不可移植的;
    • 预处理器将忽略它不认识的#pragma指令;
    • 两个不同的编译器可能以两种不同的方式解释同一条#pragma指令;

一般用法:

#pragma parameter

注:不同的parameter参数语法和意义各不相同

#pragma message

  • message参数在大多数的编译器中都有相似的实现;
  • message参数在编译时输出消息到编译输出窗口中;
  • message可用于代码的版本控制;

注意:messageVC特有的编译器指示字,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内存对齐

  1. 数据对齐Alignment
  • 结构体中不同类型的数据在内存中按照一定的规则排列,而不总是一个挨一个的顺序排放(便于寻址);
  • 任何成员变量的地址必须是其对齐参数A的整数倍,这个规则叫做数据对齐;

数据对齐会造成结构体内部不同成员变量之间有空隙;

  • 每个成员变量的对齐参数A取值规则:
    • a=min(n, max(self)) 普通类型取#pragma pack(n)、自身大小,二者最小值;
    • a=min(n, max(sizeof element...)) 结构体类型取#pragma pack(n)、最大子变量,二者最小值;

    n为系统对齐参数,一般默认为4,可以通过#pragma pack()类宏自定义

  1. 数据补齐Completion
  • 一个结构体变量的大小必须是C的整数倍,这个规则叫数据补齐;
  • 这种补齐可能造成结构体在最后多占用一些浪费的字节;
  • 结构体补齐参数C的取值规则:c=max(A) 即所有成员的对齐参数A的最大值(一定不会超过pack值,试证明之);

分析举例

// x *
// y y
// z *
typedef struct {
    char c1;  //s=1, a=min(4,1), @+0;对齐参数a=1;
    short s;  //s=2, a=min(4,2), @+2;对齐参数a=2;
    char c2;  //s=1, a=min(4,1), @+4;对齐参数a=1;
} ST1; //6 = n*2, c=max(a)==2;补齐参数c=2

// x x y y
// y y y y
// z z z z
// m * * *
typedef struct {
    short s;  //s=2, a=min(4,2), @+0;对齐参数a=2;
    ST1 st1;  //s=6, a=min(4,max(1,2,1)), @+2;对齐参数a=2;
    int i;    //s=4, a=min(4,4), @+8;对齐参数a=4;
    char c;   //s=1, a=min(4,1), @+12;对齐参数a=1;
} ST7; //16 = n*4, c=max(a)==4;补齐参数c=4;

struct成员对齐,总体补齐

  • 普通类型成员,对齐参数A为(自身类型大小和指定对齐参数n)二者的最小值,即a=min(n, max(self));
  • 结构体类型的成员,对齐参数A为其所有成员对齐参数中的最大值,即a=min(n, max(sizeof element...));
  • 结构体总长度为补齐参数C的整数倍,补齐参数C为所有成员对齐参数最大值,即c=max(A);

结构体中子变量的顺序会影响结构体的大小,占用空间小的子变量写前边可以节约内存空间;

struct test1 {
    char c1; // 1
    short s; // 2
    char c2; // 1
    int i; // 4
}; //12
struct test2 {
    char c1; // 1
    char c2; // 1
    short s; // 2
    int i; // 4
}; //8

test1和test2两种类型所占用的内存空间是否相同?

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

为什么需要内存对齐?

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

系统默认对齐参数

#pragma pack能够改变编译器的默认对齐方式

#pragma pack(n)         //设置编译器按照n个字节对齐,n可以取1,2,4,8,16
#pragma pack()          //默认4字节对齐

#pragma pack(push)      //将当前的对齐字节数压入栈顶,不改变对齐字节数
#pragma pack(push,n)    //将当前的对齐字节数压入栈顶,并按照n字节对齐
#pragma pack(pop)       //弹出栈顶对齐字节数,不改变对齐字节数
#pragma pack(pop,n)     //弹出栈顶并直接丢弃,按照n字节对齐
#pragma pack(push,1)    //可以指定结构的对齐和补齐的字节数
#pragma pack(pop)       //恢复push前的值

#include <stdio.h>
#pragma pack(push,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(pop)

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

结构体大小分析

#include <stdio.h>
//假设存储位置都起始于0
#define OFFSET_OF(type, member)  ((size_t)(&((type *)0)->member))

#define OO1(t, m1)                 #m1,OFFSET_OF(t, m1)
#define OO2(t, m1, m2)             OO1(t, m1),             OO1(t, m2)
#define OO3(t, m1, m2, m3)         OO2(t, m1, m2),         OO1(t, m3)
#define OO4(t, m1, m2, m3, m4)     OO3(t, m1, m2, m3),     OO1(t, m4)
#define OO5(t, m1, m2, m3, m4, m5) OO4(t, m1, m2, m3, m4), OO1(t, m5)

#define SHOW_OO1(t, m1)                 \
    printf("\n%s offset: %s:%ld\n",                     #t,OO1(t,m1))
#define SHOW_OO2(t, m1, m2)             \
    printf("\n%s offset: %s:%ld, %s:%ld\n",                #t,OO2(t,m1,m2))
#define SHOW_OO3(t, m1, m2, m3)         \
    printf("\n%s offset: %s:%ld, %s:%ld, %s:%ld\n",           #t,OO3(t,m1,m2,m3))
#define SHOW_OO4(t, m1, m2, m3, m4)     \
    printf("\n%s offset: %s:%ld, %s:%ld, %s:%ld, %s:%ld\n",      #t,OO4(t,m1,m2,m3,m4))
#define SHOW_OO5(t, m1, m2, m3, m4, m5) \
    printf("\n%s offset: %s:%ld, %s:%ld, %s:%ld, %s:%ld, %s:%ld\n", #t,OO5(t,m1,m2,m3,m4,m5))

#pragma pack(8)
//#pragma pack(4)
//#pragma pack(2)

typedef struct S1 {
    short a; // 2, a=min(pack, 2)
    long b;  // 8, a=min(pack, 8)
} S1;
typedef struct S2 {
    char c;    // 1,  a=min(pack, 1)
    S1 d;      // 2+8,a=min(pack, max(2,8))
    double e;  // 8,  a=min(pack,8)
} S2;

#pragma pack()

int main() {
    printf("%ld, %ld\n", sizeof(S1),  sizeof(S2));
    SHOW_OO2(S1, a, b);
    SHOW_OO3(S2, c, d, e);
    return 0;
}

运行结果

  • #pragma pack(8) 16,32; 0,8; 0,8,24;
  • #pragma pack(4) 12,24; 0,4; 0,4,16;
  • #pragma pack(2) 10,20; 0,2; 0,2,12;

#

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

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

请继续阅读后半部分:
C语言深度剖析笔记2 https://blog.csdn.net/halazi100/article/details/125844545
C语言深度剖析笔记2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值