C语言编译步骤

预处理

  • 预处理:在C语言中,带#号的符号,就是预处理指令,那么在预处理阶段,就会根据预处理指令进行预处理操作。
#include <stdio.h>
// 头文件展开:那么在预处理阶段中生成的预处理文件,就需要包含stdio.h这个文件里的信息。
// (预处理时将stdio.h的文件拷贝到预处理文件中)
  • 在预处理时生成的注释会被删除
// hello world
  • 在预处理时,宏定义会被直接替换
// C语言文件 hello.c
#define PI 3.14
....
printf(PI);
....


// 预处理生成的运处理文件(宏被替换) hello.i
printf(3.14);
  • 在预处理中不会检测语法错误。
  • 在预处理中可以进行条件编译,如 #if #endif
// 在C语言中
#if Win32
printf("win32");
#endif

// 在预处理文件中 .i文件,只要有 Win32就会printf("win32");,如果没有就不会编译这段代码

编译

将预处理文件(.i)编译生成汇编文件(.s),并且会检查语法错误

gcc -S hello.i -o hello.s

汇编

将汇编文件 编译 生成 二进制文件

gcc -C hello.s -o hello.o

链接

设置运行环境,链接其他库

gcc  hello.o -o hello.exe

System库函数

字符串

字符串大小写转换

  • 将小写变为大写
#include <stdio.h>
int main(void) {
    char a = 'a';
    a = a - 32;
    // %c 是以字符的方式在控制台输出
    printf("c = %c", a);	// 输出:A
    return 0;
}

字符串常量

字符串常量是由一对双引号括起来的字符序列。
其中'a'为字符常量、"a"为字符串常量

分析"china"字符串常量为例。

  1. C语言在每一个字符串常量的结尾加一个字符串结束标志,以便系统来判断字符串是否结束。
  2. C语言规定以字符 '\0'作为字符串的结束标志。
  3. "china"的内存单元是 6个字符,而不是5个字符。即大小为6个字节,最后一个字符为 '\0',然而在控制台输出时不输出'\0',因为'\0'在控制台不显示

混合运算

#include <stdio.h>
int main(void) {
    int value = 5;
    int res1 = value / 2;
    float res2 = (float)value / 2; // (float)value 就是将int类型的强制转化为浮点类型的
    printf("res1 = %f, res2 = %f \n", res1, res2);	// 输出:res1 = 2.500000, res2 = 0.000000
    // %f,在控制台输出浮点类型的,默认小数点后面 5 位
}

常见的数据输入/输出函数

scanf使用方法

int scanf(const char* format, ...);

  1. format是一个字符串, …是可变参数,参数的数目与format中的%数目一致
  2. %d 一个整形数、%f 一个浮点数、%c 一个单一的字符
  3. 发生错误时,返回EOF。其中 #define EOF (-1)。 什么情况下scanf发生错误。gcc编译器,行首输入ctrl + D + 回车

scanf函数原理

行缓冲:在这种情况下,当在输入和输出中遇到换行符时,将执行真正的I/O操作。这是我们输入的字符先存放到缓存区,等按下回车键换行时才进行实际的的I/O操作。典型代表是标准输入缓冲区(stdin)和标准输出缓冲区(stdout)
如上面的例子:我们向标准输入缓冲区中放入的字符为 '10\n',输入 \n(回车)后,scanf才会开始匹配。scanf函数中的%d匹配整数型10,然后放入变量value中,接着进行打印输出,这是'\n'仍然在标准输入缓冲区当中,如果第二个scanf函数为scanf("%d", &i),依然会出现阻塞。因为scanf函数在读取整数型、浮点型、字符串时,会忽略'\n'(回车符)、空格符等字符(忽略是指scanf函数执行时,会首先删除这些字符,在进行阻塞)。 如果是scanf("%c", &c)不会忽略任何字符。

  • scanf通过%s读取字符串时,在遇到空格之后,就会匹配结束,这样就没办法将一行字符串存存储到字符数组中。
#include <stdio.h>
int main(void) {
    char c1[20], c2[20];
    scanf("%s%s", c1, c2);
    printf("%s----%s", c1, c2);	
    return  0;
}
输入:
nihao helloworld
输出:
nihao----helloworld
#include <stdio.h>
int main(void) {
    int value;
    char c;
    scanf("%d", &value);
    scanf("%c", &c);
    printf("value = %d\n", value);
    printf("c = %c\n", c);
    return  0;
}
输出结果:
10		
value = 10
c = 


运行结束。

scanf函数的循环读取

#include <stdio.h>

int main(void) {
    int value;
    while ((getchar()) && scanf("%d", &value) != EOF) {
        printf("value = %d\n", value);
    }
    return  0;
}
// getchar()清空缓存区
  • 练习:输入helloworld,输出HELLOWORLD
#include <stdio.h>
int main(void) {
    char c;
    while (scanf("%c", &c) != EOF) {
        if(c != '\n'){
            printf("%c", c - 32);
        }else {
            printf("\n");
        }
    }
    return  0;
}

多种数据类型的混合输入

混合输入时,每次在%c之前加入一个空格

#include <stdio.h>

int main(void) {
    int i;
    float f;
    char c;
    scanf("%d %c%f", &i, &c, &f);	// 输入:5 m 98.5
    // %4.1f 表示占4个空格(包括小数点),并且显示小数点后面1位
    printf("i = %d, c = %c, f = %4.1f", i,c,f); //输出:i = 5, c = m, f = 98.5
    return  0;
}

C数组

一维数组

// 1. 声明数组
double c[10];

// 2. 初始化数组,用大括号初始化
double c[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

// 3. 访问数组
c[9];

// 其中c就是c[0]的地址

一维数组在内存中的存储

数组里面其实存储的是地址。如果修改数组里的值,会先去找到地址,通过地址去找到值,在进行修改。

字符数组

// 定义数组并初始化(实际不会采取这种方式)
char c[10] = {'h', 'e', 'l', 'l', 'o'};

// 实际中
// c[10]最长存储9个字符,最后一个字符来存储 '\0'
char c[10] = "hello";
  • 有趣的测试
#include <stdio.h>
int main(void) {
    char c[5] = {'h', 'e', 'l', 'l', 'o'};
    char d[5] = "how";
    printf("%s----%s\n", c,d); // 输出:hello  JrU----how
    // 为什么会输出 hello JRU呢?
    // c[5]最多存储4个字符,如果存储5个字符会把结束符替换掉
    // 因为c没有结束符,它只有结束符'\0'才会结束输出
    return  0;
}

gets函数和put函数

gets函数从STDIN(标准输入)读取字符并把它们加载到str(字符串中),直到遇到换行符\n或到达EOF。比如说:我们在控制台输入 how are you 回车,这里其实有11个字符,包括空格,并且最后有'\0',不会存储'\n'

由于安全性问题,gets被遗弃了

puts函数就是将字符串写入到标准输出STDOUT中。在控制台显示

#include <stdio.h>
int main(void) {
    char c1[20], c2[20];
    gets(c1);	// 输入 how are you 回车
    puts(c1);  // 输出 how are you
    return  0;
}

str系列的函数

需要引用 #include <string.h>

  • size_t strlen (const char *__s)
    定义:计算字符串的长度
    注意:长度不会计算 '\0'
#include <stdio.h>
#include <string.h>
int main(void) {
    char c1[20] = "hello";
    printf("字符数组c1的长度为%d", strlen(c1));
    // 输出: 字符数组c1的长度为5
    return 0;
}
  • char *strcpy (char *dest, const char *src)
    定义:复制src到dest
    注意:有const修饰的表示这个地方可以放一个字符串常量
#include <stdio.h>
#include <string.h>
int main(void) {
    char str[20];
    strcpy(str, "helloworld");
    puts(str);	// 输出 helloworld
    return 0;
}
  • int strcmp (const char *__s1, const char *__s2)
    定义:比较两个字符串的大小,比较的是ASCII码,如果相等为0,大于为1,小于为-1
#include <stdio.h>
#include <string.h>
int main(void) {
    printf("比较两个字符串的大小为%d\n", strcmp("hello", "hello")); // output 比较两个字符串的大小为0
    printf("比较两个字符串的大小为%d\n", strcmp("how", "hello")); // output 比较两个字符串的大小为1
    return 0;
}
  • char *strcat (char * __dest, const char * __src)
    定义:拼接字符串,将src字符串拼接到dest字符串的后面
    注意:_dest数组必须容纳拼接之后数组的大小
#include <stdio.h>
#include <string.h>
int main(void) {
    char str[20] = "hello";
    puts(strcat(str, "world"));
    return 0;
}

指针

指针的本质就是地址

指针初始化

#include <stdio.h>
#include <string.h>
int main(void) {
    int i = 5;
    // & 取地址
    int *p = &i;
    printf("%d\n", i); // 直接访问
    printf("%d\n", *p); // 间接访问
    return 0;
}

指针的传递

#include <stdio.h>
#include <string.h>

void changeI(int* p){	// 形参  p = &i;
    *p = 25;
}

int main(void) {
    int i = 5;
    changeI(&i); // 实参
    printf("i = %d\n", i);//output  i = 25
    return 0;
}

指针的偏移

#include <stdio.h>
#include <string.h>

int main(void) {
    int str[5] ={1, 2, 3,4,5};
    int * p = str; // 数组名就是这个数组的起始地址
    int i = 0;
    for(i = 0; i < 5; i++) {
        printf("%d\t", *(p + i)); // output 1       2       3       4       5
    }
}

指针与自增、自减运算符

#include <stdio.h>
#include <string.h>

int main(void) {
    int arr[5] = { 2, 3, 5, 8, 10};
    int *p = arr;
    注意:任何时候都先把后++去掉,第二步才会算后++
    int j = *p++; // int j = *p; p++;
    printf("arr[0] = %d, j = %d, *p = %d\n", arr[0], j, *p); // arr[0] = 2, j = 2, *p = 3

    return 0;
}
#include <stdio.h>
#include <string.h>
int main(void) {
    int arr[5] = { 2, 3, 5, 8, 10};
    int *p = arr;
    int j = (*p)++; // int j = *p; (*p)++;
    printf("arr[0] = %d, j = %d, *p = %d\n", arr[0], j, *p); // arr[0] = 3, j = 2, *p = 3
    return 0;
}

指针与一维数组

一维数组的数组名就是地址,是起始地址.

#include <stdio.h>
int main(void) {
    int arr[5] = { 2, 3, 5, 8, 10}; 
    int *p = arr;	// arr 就代表了地址
    printf("*p=%d\n", *p);  // *p=2
    int j = *p++;
    printf("j= %d\n", j);   // j= 2
    printf("*p=%d\n", *p);   // *p=3

}

指针与动态内存申请(重要)

动态内存申请需要引入#include <stdlib.h>

// 从对空间中申请多少个字节的空间
// __size就是字节大小
void *malloc (size_t __size);
  • 栈空间和堆空间的差异
    栈空间会随着函数执行的结束而释放。

字符指针和字符数组的初始化

#include <stdio.h>
int main(void) {
    char * p = "hello"; // 将字符串型常量的首地址赋给p
    char c[10] = "hello";   // 等价于 strcpy(c, "hello");
    c[0] = 'H'; 
    p[0] = 'H'; // 非法。 不可以对字符串常量数据进行修改
    p = "world";    // 将字符串world的地址赋值给p
    c = "world";    // 非法 (数组名并不是一个变量)
}

结构体

1. 结构体的使用

#include <stdio.h>

// 声明一个结构体
struct student {
    int num;    // 结构体成员
    char name[20];
    char sex;
    int age;
    float score;
}; 

int main(void) {
	// 初始化结构体 struct student就可以理解为一个数据结构,如int、float等
    struct student s = {12, "libai", '0', 15, 100.0};
    // 通过 . 的方式,来获取结构体成员
    printf("%d %s %c %d %5.2f", s.num, s.name, s.sex, s.age, s.score);
    return 0;
}

2. 结构体数组

#include <stdio.h>

// 声明一个结构体
struct student {
    char name[20]; // 结构体成员
    char sex;
    int age;
}; 

int main(void) {
    struct student s[3];
    int i = 0;
    for(i = 0; i < 3; i++) {
    	// 使用scanf时,遇到字符时,前需要加一个空格
        // 为什么?详见上面
        scanf("%s %c%d", s[i].name, &s[i].sex, &s[i].age);
    }

    for(i = 0; i < 3; i++) {
        printf("%s %c %d\n", s[i].name, s[i].sex, s[i].age);
    }

    return 0;
}

3. 结构体大小

struct teacher{
    int age; // 4
    char sex; // 1 
    char name[5];  // 3 + 4   要对齐
};

int main(void) {
    printf("%lu\n", sizeof(struct teacher)); 
    return 0;
}

4. 结构体指针

#include <stdio.h>

struct teacher{
    int age;  // 4
    char sex;  // 1 
    char name[25];  // 3 + 24
};

int main(void) {
    struct teacher t = {18, 'm', "wangli"};
    struct teacher *p = &t;
    // 方式一:因为 . 的优先级比 * 高,所以要加括号
    printf("%d %c %s\n", (*p).age, (*p).sex, (*p).name);
    // 方式二:使用指针符号 ->
    printf("%d %c %s\n", p->age, p->sex, p->name);
    return 0;
}

5. 对结构体的思考

#include <stdio.h>

struct teacher{
    int age;
    char sex;
    char name[25];
};

int main(void) {
    struct teacher t[3] = {18, 'm', "wangli", 19, 'f', "lisi"};
    struct teacher *p = t;
    int age;
    age = p->age++; // 1. age = p->age; 2. p->age++;
    printf("age=%d, p->age=%d\n", age, p->age);   // 18,19
    age = p++->age; // 1. aeg = p->age; 2. p++;
    printf("age=%d, p->age=%d\n", age, p->age);   // 19, 19

    return 0;
}

typedef的使用

typedef的作用就是起别名

如下列中
teacher 等价于 struct teacher
p 等价于 struct teacher*
如果我们声明变量,如下:
teacher t1 = { 18, '5', "lisi"}
p t2 = &t1

typedef struct teacher{
    int age;
    char sex;
    char name[25];
}teacher, *p;

C++引用

C++引用是一种内存地址别名,它允许我们使用一个名称来引用另一个变量。引用通常用作函数参数和返回值类型,以便可以避免复制大型数据结构。

在C++中,使用&符号来声明一个变量的引用。例如,以下代码定义了一个整数变量a,并为其创建了一个引用b:

int a = 5;
int &b = a;

这将创建一个名为b的引用,它指向变量a的内存地址。现在,如果我们修改b的值,实际上是在修改a的值,因为b和a共享同一块内存空间。例如

b = 10; // 现在 a 的值也变成了 10

注意,引用必须在定义的同时初始化,且不能被重新绑定到另一个对象。此外,引用可以被用作函数参数,以便可以在函数内部操作原始变量而不需要进行复制。

C++引用用于函数参数

引用通常被用作函数参数,以便可以在函数内部操作原始变量而不需要进行复制。通过引用传递参数比通过值传递参数更高效,因为它避免了在函数调用时的数据拷贝。

void increment(int& x) {
    x++;
}

这个函数接受一个整数的引用,并将其值增加1。现在,如果我们调用该函数并传递一个整数变量作为参数,该变量的值将被修改:

int a = 5;
increment(a); // 现在 a 的值变成了 6

请注意,当我们将引用作为参数传递给函数时,实际上是将原始变量的内存地址传递给了函数。因此,在函数内部对引用所引用的变量的任何修改都会影响到原始变量