C语言——学习笔记(全)

一、语言入门基础

1.输出函数说明

printf函数

​ 头文件:stdio.h

​ 原型:int printf(const char *format, …);

​ format:格式控制字符串

​ …:可变参数列表

​ 返回值:成功输出字符的数量

2.输入函数说明

scanf函数

​ 头文件:stdio.h

​ 原型:int scanf(const char *format, …);

​ format:格式控制字符串

​ …:可变参数列表

​ 返回值:成功读入的参数个数

int a = scanf("hello"); //a = 0

​ 循环读入:【EOF==-1】

while(scanf(...) != EOF){} //CTRL+C:强制结束一个进程;CTRL+Z:-1/EOF

使用printf函数,循环读入数字,求解一个非负整数n的十进制表示的数字位数

#include<stdio.h>

int main(){
    int n;
    while(scanf("%d", &n) != EOF){
    	if(n < 0)
    	{
    		printf("请输入一个非负整数!");
    		continue;
    	}
        printf(" has %d digits!\n", printf("%d", n));
    }

    return 0;
}

读入一行字符串,可能包含空格,输出这个字符串中字符的数量

#include<stdio.h>

int main(){
    char str[1000] = {0};
    //正则表达式:^\n:除了换行符以外的字符
    while(scanf("%[^\n]s", str) != -1){//str:字符数组首地址
        getchar();//强制吞掉输入流中的一个字符
        printf(" has %d chars!\n", printf("%s", str));
     }

    return 0;
}

输入输出家族的常用操作

#include<stdio.h>

int main(){
    //stdin ==> 0
    //stdout ==> 1
    //stderr ==> 2
    int a, b, c, d;
    char str[1000] = {0};
    int ans[4] = {0};
    scanf("%d%d%d%d", &a, &b, &c, &d);
    sprintf(str, "%d.%d.%d.%d", a, b, c, d);//字符串拼接
    printf("str = %s\n", str);

    sscanf(str, "%d.%d.%d.%d", &ans[0], &ans[1], &ans[2], &ans[3]);
    for(int i = 0; i < 4; i++){
        printf("%d ", ans[i]);
    }
    printf("\n");
    FILE *fin = fopen("./output", "w");
    fprintf(fin, "str = %s\n", str);
    fclose(fin);

    char str_out[1000] = {0};
    FILE *fout = fopen("./output", "r");
    fscanf(fout, "%[^\n]s", str_out);
    printf("%s\n", str_out);
    fclose(fout);


    return 0;
}

w+知识点补充:

#include<stdio.h>

int main(){
    char str[100] = "hello kaikeba 123@qq.com";
    char temp[100] = {0};
    FILE *fp = fopen("./output", "w+");//w+:先清空内容,如有写入操作再写入,后读取
    fprintf(fp, "%s", str);
    fseek(fp, 0, SEEK_SET);//调整指针光标到首位,因为写入的时候光标已经到最后面了
    fscanf(fp, "%[^\n]s", str);
    fclose(fp);
    printf("%s\n", str);

    return 0;
}

二、数学运算

1.C语言基本运算符

类型转换:

​ 显式类型转换——强制转换

​ 隐式类型转换——自动类型转换

2.位运算:效率最高【&、|、^、~ 优先级同级】

​ ①二进制下进行的计算

&:

​ ①单目运算符:取地址;

​ ②双目运算符:按位与

%2 == &1
%4 == &3
%6 =X= 不等于任何值
%8 == &7
%16 == &15

|:按位或(有1则为1,全0才为0)

^:异或(相同为0,不同为1)

逆运算:满足交换律(a+b <==> b+a)

a + b = c ==> c - b = a | c - a = b ==> -+的逆运算
a - b = c ==> c + b = a | c + a != b ==> +不是-的逆运算

也是一类逆运算,运算符是^运算符的逆运算

a ^ b = c ==> c ^ b = a | c ^ a = b
a ^ a = 0 //相同为0
n ^ 0 = n //不同为1

面试题:有一串数字,每个数字都出现了两次,其中只有一个值x只出现了一次,请快速查找处这个值

​ 将每个值之间做异或,最后的答案就是这个值

12323
1 ^ 2 ^ 3 ^ 2 ^ 3 = 1

进阶:有一串数字,每个数字都出现了两次,其中有n个值x只出现了一次,请快速查找处这个值

先排序后异或即可

~:按位取反(二进制每一位取反,符号位为1是负数)

原码:
正数是其二进制本身;
负数是符号位为1,数值部分取X绝对值的二进制。
反码:
正数的反码和原码相同;
负数是符号位为1,其它位是原码取反。
补码:
正数的补码和原码,反码相同;
负数是符号位为1,其它位是原码取反,未位加1。(反码末尾减1)(或者说负数的补码是其绝对值反码未位加1)
取反就是简单的 01,10 ;
而按位取反需要涉及以上概念。要弄懂这个运算符的计算方法,首先必须明白二进制数在内存中的存放形式,二进制数在内存中是以补码的形式存放的。

下面以计算正数 9 的按位取反为例,计算步骤如下(注:前四位为符号位):

- 原码   : 0000 1001
- 算反码 : 0000 1001 (正数反码同原码)
- 算补码 : 0000 1001 (正数补码同反码)
- 补取反 : 1111 0110 (全位0110- 算反码 : 1111 0101 (末位减1- 算原码 : 1111 1010 (其他位取反)

总结规律: ~x = -(x+1)

补码=~原码+1

-1:
原码: 1000 0000 .... 0001
~原码:1111 1111 .... 1110   (除了符号位按位取反)
					  +1
补码: 1111 1111 .... 1111

3.左移和右移

左移:低位补0【相当于乘2】

右移:高位补符号位【相当于除2】

总结:

1.%是速度最慢的 %2 == &1
2.整型值、字符类型才能进行位运算

4.C语言中的数学函数库

头文件:math.h
常用函数:
pow(a, n) fabs(n) sqrt(n) log(n) ceil(n) log10(n) floor(n) acos(n) abs(n)(stdlib.h)

pow函数:指数函数
头文件:math.h
原型:double pow(double a, double b);
a:底数
b:指数
返回值:返回a的b次方的结果
例子:pow(2, 3) = 8 [这里的8是一个double类型值]

sqrt函数:开平方函数
头文件:math.h
原型:double sqrt(double x);
x:被开方数
返回值:返回根号x的结果
例子:sqrt(16) = 4 [这里的4是一个double类型值]

ceil函数:上取整函数(天花板函数)
头文件:math.h
原型:double ceil(double x);
x:某个实数
返回值:返回x向上取整的结果
例子:ceil(4.1) = 5 [这里的5是一个double类型值]

floor函数:下取整函数(地板函数)
头文件:math.h
原型:double floor(double x);
x:某个实数
返回值:返回x向下取整的结果
例子:floor(4.9) = 4

abs函数:整数绝对值函数
头文件:stdlib.h
原型:int abs(int x);
x:某个整数
返回值:返回x的绝对值的结果
例子:abs(-4) = 4

fabs函数:实数绝对值函数
头文件:math.h
原型:double fabs(double x);
x:某个实数
返回值:返回x的绝对值的结果
例子:fabs(-4.5) = 4.5

log函数:以e为底对数函数
头文件:math.h
原型:double log(double x);
x:某个实数
返回值:返回log以e为底的x的结果
例子:log(9) = 2.197225…

log10函数:以10为底的对数函数
头文件:math.h
原型:double log10(double x);
x:某个实数
返回值:返回log以10为底的x的结果
例子:log10(1000) = 3

acos函数:反三角函数(反余弦)
头文件:math.h
原型:double acos(double x);
x:角度的弧度值
返回值:返回arccos(x)的结果
例子:acos(-1) = 3.1415926… [sin(90°)要写sin(Π/2)]

int类型使用32个bite去存储,其中最左边的为从右往左数的第32位,是符号位,用于存储符号
有符号位:int:[-2^31, 2^31 - 1]
无符号为:int:[0, 2^32 - 1]


#include<stdio.h>

int main(){
    int a, b;

	/*scanf返回值为-1时,二进制的-1为32个1,按位取反得到32个0,结果为0*/
    while(~scanf("%d%d", &a, &b)){
        printf("a = %d, b = %d\n", a, b);
        //一般使用异或运算时要先判断a与b是否相等,否则容易出现交换错误
        a ^= b;
        b ^= a;
        a ^= b;
        printf("a = %d, b = %d\n", a, b);
    }
    
    return 0;
}

三、流程顺序控制方法

a = 3, b = -3, c = 0; ==> 逻辑表达式返回的是最后一个逗号表达式的值,0
!!(x) ==> 逻辑归一化 (真值有很多:1 2 3 4... 假值也有:0 null 空地址 '\0')将真假归一为01

1.分支结构

1.If语句

if(表达式){ //判断表达式的逻辑返回值是否为真
    代码段;
}
if(表达式){
    代码段1;
} else {
    代码段2;
}
if(表达式1){
    代码段1;
} else if(表达式2){
    代码段2;
} else {
    代码段3;
}

if(表达式) + 一条语句:

​ ①空语句

​ ②单语句 (以分号表示结尾)

​ ③复合语句(以{}表示)

2.switch语句

swithc(a){ //a只能是整数值。(字符型==>ASCII码)
    //case条件入口,程序进入case所对应的代码段
    //依次执行后续代码知道遇到break或者switch结构结尾
    case 1:代码块1;
    case 2:代码块2;
    ......
    default: 代码块n; //相当于else语句,如果以上情况都没有就执行这里
}

3.三目运算符

#include<stdio.h>

int main(){

    int n;
    scanf("%d", &n);

    if(n >= 0) printf("非负数");
    else printf("负数");

    printf("%s\n", n >= 0 ? "非负数" : "负数");

    return 0;
}

四、循环结构

1.while语句

while(表达式){ //每当表达式为真,代码块就会被执行一次
    代码块;//最少执行0次
}
do{
    代码块; //最少执行1次
} while(表达式); //每当代码块执行一次,就会判断一次表达式是否为真

在进行浮点数的判断时,不建议直接使用==。浮点数之间其实是没有绝对的相等的。解决浮点数判等问题——设置精度(epl),当|a - b| < epl的时候,可以理解为是相等的

2.for语句

//①初始化,只在第一次被执行
//②循环条件判断,为真才循环
//③执行代码块
//④执行后操作
//跳转到②
for(初始化;循环条件;执行后操作){
    代码块;
}

3.练习题

(1)回文整数

bool isPalindrome(int x){
    //使用__builtin_expect()有可能加速
    if(__builtin_expect(!!(x < 0), 0)) return false; //判断x是否小于0
    int y = 0, z = x;
    while(x){
        y = y * 10 + x % 10;
        x /= 10;
    }
    return z == y;
}

CPU的分支预测

早期:串行执行指令(①外存 ②内存) 问题:CPU要等待执行

​ 1条指令需要5个时钟周期,五条指令需要5 * 5 = 25个时钟周期

现在:并行执行指令 车间流水线

​ 并行:五条指令需要5 + 4 = 9个时钟周期

CPU的分支预测会根据以往执行的经验去进行预测该加载哪一项,如果发现不对,CPU有的容错机制会将预测的情况全部推翻,重新加载,重新计算,这个操作相当于CPU执行了一次串行执行。

if(yes) seg1 //如果以前走的大多数情况是这条路,预测也会走这条路
else seg2

在不影响代码可读性的情况下尽可能的减少分支结构

以下代码为帮助CPU进行分支预测:

#define likely(x) __builtin_expect(!!(x), 1) //likely代表x经常成立
#define unlikely(x) __builtin_expect(!!(x), 0) //unlikely代表x不经常成立

拓展:

__builtin_ffs(x): 返回x中最后一个为1的位是从后向前的第几位
__builtin_popcount(x): x中1的个数
__builtin_ctz(x): x末尾0的个数。x = 0时结果未定义
__builtin_clz(x): x前导0的个数。x = 0时结果未定义
__builtin_prefetch(const void *addr, ...): 怼数据手工预取的方法
__builtin_types_compatible_p(type1, type2): 判断type1和type2是否是相同的数据类型
__builtin_expect(long exp, long c): 用来引导gcc进行条件分支预测

__builtin_constant_p(exp): 判断exp是否在编译时就可以确定其为常量
__builtin_parity(x): x中1的奇偶性
__builtin_return_address(n): 当前函数的第n级调用者的地址

(2)分支结构代码演示

#include<stdio.h>
#include <time.h>
#include <stdlib.h>

int main(){
   int a = 0, b = 0;
   if((a++) && (b++)){ //&&的短路运算规则:第一个逻辑表达式返回值如果为假不再执行后面的表达式
       printf("true\n");
   } else {
       printf("false\n");
   }

   printf("a = %d, b = %d\n", a, b); 

   //随机生成n个整型值,打印n个值
   //控制输出格式:判断第一个值
   //统计其中有多少个奇数值
   srand(time(0)); //初始化一个随机种子,一直变化的值,不断累加
   printf("%ld\n", time(0));//当前Linux系统的时间戳,从1970年1月1日开始至今
   int n, cnt = 0;
   scanf("%d", &n);
   for(int i = 0; i < n; i++){
       int val = rand() % 100; //在计算机中目前无法真正生成随机数字。(伪随机)
       //if(i) printf(" ");
       i && printf(" ");
       printf("%d", val);
       //if(val & 1) ++cnt;
       cnt += (val & 1);
   }
   printf("\ncnt: %d\n", cnt);

   return 0;
}

(3)循环结构代码演示

#include<stdio.h>

int reverse_num(int n){
    int x = n, temp = 0;
    while(x){
        temp = temp * 10 + x % 10;
        x /= 10;
    }
    return temp == n;
}

int main(){
    int n;
    scanf("%d", &n);

    for(int i = 1; i <= n; i++){
        if(!reverse_num(i)) continue;
        printf("%d\n", i);
    }

    return 0;
}

五、函数

1.基本函数定义与用法

int functionName(int); //函数的声明:说明
//函数的定义:具体的实现
int functionName(int x){ //int:返回值	functionName:函数名  int x:参数声明列表
    //实现代码
    return (int)x; //返回函数调用
}
#include<stdio.h>

void func();//函数声明

int main(){ //主函数:默认情况下的程序入口
    //func();
    printf("main: test!\n");

    return 0; // 对操作系统来说,0表示真值
}

__attribute__((constructor)) //预处理命令。会让此函数先于主函数执行
void func(){
    int a = 2, b = 3;
    printf("func: %d * %d = %d\n", a, b, 2 * 3);
    return ;
}

__attribute__((constructor)) //顺序执行
void test(){
    printf("hello world!\n");
}

2.K&R风格的函数定义

int is_prime(x);
int x;{
    for(int i = 2; i * i <= x; i++){
        if(x % i == 0) return 0;
    }
    return 1;
}

3.递归函数

程序调用自身的编程技巧叫做【递归】,而不是算法(递推)

递归程序的组成部分:

(1)给予合理的语义信息

(2)边界条件处理

(3)针对问题的 处理过程递归过程(分治思想)

(4)结果返回 ①return ②全局变量 ③传出参数(指针)

递归需要更多的额外时间开销

系统栈:(函数内部调用的空间都占用系统栈)

​ Linux:8MB

​ Window:2-8MB

函数内部的局部变量只有在调用的时候才分配空间

4.函数指针

​ 指针变量也是变量,用于接收地址

//前三个参数就是函数指针变量。*可以理解为指针变量的标志。int: 函数返回值,(*f1):f1为函数指针变量(int):函数的参数列表
int g(int (*f1)(int), int (*f2)(int), int (*f3)(int), int x){
    if(x < 0) return f1(x);
    if(x < 100) return f2(x);
    return f3(x);
}

函数是压缩的数组,数组是展开的函数

#include<stdio.h>
#include <inttypes.h>

int64_t Triangle(int64_t n){//三角形
    return n * (n + 1) / 2;
}
int64_t Pentagonal(int64_t n){//五边形
    return n * (3 * n - 1 ) >> 1;
}
int64_t Hexagonal(int64_t n){//六边形
    return n * (2 * n - 1);
}
//函数式
int64_t binary_search1(int64_t (*arr)(int64_t), int64_t n, int64_t x){
    int64_t head = 1, tail = n, mid;
    while(head <= tail){
        mid = (head + tail) >> 1;
        if(arr(mid) == x) return 1;
        if(arr(mid) < x) head = mid + 1;
        else tail = mid - 1;
    }
    return 0;
}
//数组式
int64_t binary_search(int64_t *arr, int64_t n, int64_t x){
    int64_t head = 0, tail = n - 1, mid;
    while(head <= tail){
        mid = (head + tail + 1) >> 1;
        if(arr[mid] == x) return mid;
        if(arr[mid] < x) head = mid + 1;
        else tail = mid - 1;
    } 
    return -1;
}
int64_t main(){
    int64_t n = 143;
    while(1){
        //int64_t temp = Triangle(++n);
        int64_t temp = Hexagonal(++n);
        if(!binary_search1(Pentagonal, temp, temp)) continue; //判断五边形
        printf("%ld\n", temp);
        break;
    }

    return 0;
}

5.欧几里得算法

辗转相除法。

用于快速计算两个数字的最大公约数

还可以用于快速求解a * x + b * y = 1的方程的一组整数解

定理:a和b两个整数的最大公约数等于b与a % b的最大公约数。
形式化表示:假设a,b != 0, 则gcd(a, b) = gcd(b, a % b)
证明1:
1.设c = gcd(a, b),则a = cx, b = cy
2.可知a % b = r = a - k * b = cx - kcy = c(x - ky)
3.可知c也是r的因数
4.其中x - ky与y互素。见证明2
所以可知:gcd(a, b) = gcd(b, r) = gcd(b, a % b)
证明2:
1.假设gcd(x = ky, y) = d
2.则y = nd, x - ky = md, 则x = knd + md = d(kn + m)
3.重新表示a, b,则有a = cd(kn + m),b = cdn
4.则可得gcd(a, b) >= cd, 又因为gcd(a, b) = c,所以d = 1

最大公约数:b == 0 ? a : gcd(b, a % b)

最小公倍数:a * b / gcd(a, b)

#include<stdio.h>

int gcd(int , int);

int lcm(int a, int b){
    return a * b / gcd(a, b);
}

int gcd(int a, int b){
   // if(!b) return a;
   // return gcd(b, a % b);
    return b == 0 ? a : gcd(b, a % b);
}

int main(){
    int a, b;
    while(~scanf("%d%d", &a, &b)){
        printf("gcd(%d, %d) = %d\n", a, b, gcd(a, b));
        printf("lcm(%d, %d) = %d\n", a, b, lcm(a, b));
    }

    return 0;
}

6.变参函数

实现可变参数max_int,从若干个传入的参数中返回最大值。

	int max_int(int a, ...); //至少要知道一个参数的名字

如何获得a往后的参数列表?va_list类型的变量
如何定位a后面第一个参数的位置?va_start “函数”  (初始化)
如何获取下一个可变参数列表中的参数?va_arg “函数”  (指明了类型)
如何结束整个获取可变参数列表的动作?va_end函数  (释放)
#include<stdio.h>
#include <inttypes.h>
#include <stdarg.h>

int max_int(int n, ...){
    int ans = INT32_MIN;//32位整型极小值
    va_list arg;
    va_start(arg, n); //n后面的值赋值给arg(不包括n,n代表一共有几个数)
    while(n--){
        int temp = va_arg(arg, int); //int表示下一个参数必须是整型的
        if(temp > ans) ans = temp;
    }
    va_end(arg);
    return ans;
}

int main(){
    printf("%d\n", max_int(3, 2, 0, 3));
    printf("%d\n", max_int(5, 20, 10, -3));//少传几个数不影响
    printf("%d\n", max_int(3, 2, 0, 3, 8, 9, 10)); //多传几个数,只会取前n个

    return 0;
}

六、数组与预处理命令

1.数组声明与初始化

int a[2]; //声明
//初始化
int a[2] = {1};
int a[2] = {1, 2};

数组是具有相同类型变量的集合。

​ 数组下标从0开始。

​ 数组的地址在内存上是连续的。

​ 数组本身是支持随机访问的。

2.素数筛

1.标记一个范围内的数字是否是合数,没有被标记的则为素数
2.算法的空间复杂度为O(n),时间复杂度为O(n*loglogn) ~= O(n)
3.总体思想是用素数去标记掉不是素数的数字,例如知道了i是素数,那么2i、3i、4i...就都不是素数

(1)基本方法判断素数

#include<stdio.h>
#include <math.h>

int is_prime(int n){
    for(int i = 2, ans = sqrt(n); i <= ans; i++){
        if(n % i == 0) return 0;
    }
    return 1;
}

int main(){

    for(int i = 2; i < 100001; i++){
        if(!is_prime(i)) continue;
        printf("%d\n", i);
    }

    return 0;
}

(2)素数筛

#include<stdio.h>
#define MAX_N 1000

int prime[MAX_N + 10] = {0, 1, 0};

void init_prime(){
    for(int i = 2; i <= MAX_N; i++){
        if(prime[i]) continue;
        prime[++prime[0]] = i; //节约空间,存储目前已知的素数,prime[0]用于计数
        for(int j = i, J = MAX_N / i; j <= J; j++){//从i开始枚举减少重复标记
            prime[i * j] = 1;
        }
    }
    return ;
}

int main(){

    init_prime();

    for(int i = 1; i <= prime[0]; i++){
        printf("%d\n", prime[i]);
    }

    return 0;
}

3.折半查找(二分查找)

【要满足单调性】

#include<stdio.h>

int binary_search(int *arr, int n, int x){
    int head = 0, tail = n - 1, mid = 0;
    while(head <= tail){
        mid = (head + tail) >> 1;
        if(arr[mid] == x) return mid;
        if(arr[mid] < x) head = mid + 1;
        else tail = mid - 1;
    }
    return -1;
}

int main(){

    int n, x, arr[100] = {0};
    scanf("%d", &n);
    
    for(int i = 0; i < n; i++) scanf("%d", &arr[i]);

    while(~scanf("%d", &x)){
        printf("%d\n", binary_search(arr, n, x));
    }

    return 0;
}
#include<stdio.h>
#include <math.h>

//O(logn)
double my_sqrt(double n){
    //当n < 1,sqrt(n) > n,所以要加上1.0
    double head = 0, tail = n + 1.0, mid = 0; 

    #define EPSL 1e-8 //1*10^-8  定义精度值

    while(tail - head > EPSL){
        mid = (head + tail) / 2.0;
        if(mid * mid < n) head = mid;
        else tail = mid;
    }

    #undef EPSL //结束宏定义
    return head;
}

int main(){
    double n;
    while(~scanf("%lf", &n)){
        printf("sqrt(%lf) = %lf\n", n, sqrt(n)); //用的牛顿迭代法,更优
        printf("my_sqrt(%lf) = %lf\n", n, my_sqrt(n));
    }

    return 0;
}

4.数组的相关代码讲解

#include<stdio.h>

//void func(int num[]){
void func(int *num){ //num为指针变量,而不是数组
    num[0] = 1234;
    printf("func: num = %p\tnum+1 = %p \n"); //表现形式一致性,外部调用和内部的地址一致(表现 形式一致)
}

//void func1(int num[][20]){ //只能省略一个维度
void func1(int (*num)[20]){
    num[0][0] = 1234;
    printf("func: num = %p, num + 1 = %p\n", num, num + 1);
}

int main(){
    int arr[100];
    double num[100];
    printf("%lu\n", sizeof(arr));
    printf("arr = %p\tarr[0] = %p\n", arr, &arr[0]);
    printf("arr + 1 = %p\n", arr + 1);
    printf("num = %p\tnum[0] = %p\n", num, &num[0]);
    printf("num + 1 = %p\n", num + 1);

    func(arr);
    printf("arr[0] = %d\n", arr[0]);

    printf("==========================================================\n");

    int arr1[100][20]; //100行20列
    printf("arr = %p, arr + 1 = %p\n", arr, arr + 1);
    int **p; //指针p指向的是另一个指针变量
    printf("p = %p, p + 1 = %p\n", p, p + 1); //在64位操作系统中一个指针变量默认8字节
    func1(arr1);


    return 0;
}

5.字符串(字符数组)

C语言中不存在字符串这个类型变量,只有字符数组

定义字符数组:
    char str[size];
初始化字符数组:
	char str[] = "hello world"; //12个字节,最后结尾是:'\0'(ASCII码值是0)
	char str[size] = {'h', 'e', 'l', 'l', 'o'};
#include<stdio.h>

int main(){
    char str[] = "hello world"; //自动添加'\0'
    char str1[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'}; //不会自动添 加'\0'

    printf("%s\n", str);
    printf("sizeof(str) = %lu\n", sizeof(str));
    printf("%s\n", str1);
    printf("sizeof(str1) = %lu\n", sizeof(str1));

    return 0;
}

字符串的相关操作

头文件:string.h

strlen(str); //计算字符串长度,以'\0'作为结束符
strcmp(str1, str2); //字符串比较
strcpy(dest, src); //字符串拷贝
strncmp(str1, str2, n); //安全的字符串比较
strncpy(str1, str2, n); //安全的字符串拷贝
memcpy(str1, str2, n); //内存拷贝,n的单位是字节
memcmp(str1, str2, n); //内存比较,n的单位是字节
memset(str1, c, n); //内存设置,n的单位是字节

头文件:stdio.h

sscanf(str1, format, ...); //从字符串str1读入内容
sprintf(str1, format, ...); //将内容输出到str1中

使用字符串相关操作方法,计算一个整型值16进制的表示的位数

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

int main(){
    char str[11];
    int n;
    while(scanf("%d", &n) != EOF){
        sprintf(str, "%X", n);
        printf("%d(%s) has %lu digits\n", n, str, strlen(str));
    }

    return 0;
}

6.预处理命令——宏定义

宏定义只进行符号替换,不进行相关计算

//定义符号常量:
#define PI 3.1415926
#define MAX_N 10000
//定义傻瓜表达式
#define MAX(a, b) (a) > (b) ? (a) : (b)
#define S(a, b) a * b //S(2+3, 5+6) = 2 + 3 * 5 + 6 = 23
//定义代码段
#define P(a){\
	printf("%d\n", a);\
}

7.预处理命令——预定义的宏

__DATE__ //日期:M mm dd yyyy
__TIME__ //时间:hh:mm:ss
__LINE__ //行号 (int)
__FILE__ //文件名 (xxx.c)
__func__ //函数名/非标准
__FUNC__ //函数名/非标准
__PRETTY_FUNCTION__ //更详细的函数信息/非标准

8.预处理命令——条件式编译

#ifdef_DEBUG //是否定义了DEBUG宏
#ifndef_DEBUG //是否没定义DEBUG宏
#if_MAX_N == 5 //宏MAX_N是否等于5
#elif MAX_N == 4 //否则宏MAX_N是否等于4
#else
#endif

C源码—预处理阶段(替换所有预处理命令)—>编译源码(编译期)------>对象文件(中间文件linux: xx.o | window: xx.obj)—链接中间文件—>可执行程序(linux: a.out | windows: .exe)


请实现一个没有BUG的宏(判断a的大小),需要通过如下测试:

1.MAX(2, 3)

2.5 + MAX(2, 3)

3.MAX(2, MAX(3, 4))

4.MAX(2, 3 > 4 ? 3 : 4)

5.MAX(a++, 6) a的初始值为7,函数返回值为7,a的值变为8

使用gcc xxx.c -E 能看到预编译期宏替换之后的代码

#include<stdio.h>

//typeof()替换变量,这里相当于int。__a只是一个名字,可以为其他
#define MAX(a, b) ({ \
    typeof(a) __a = a; \
    typeof(b) __b = b; \
    __a > __b ? __a : __b; \
})

// ((a) > (b) ? (a) : (b))

//#是字符串化,将传进来的func原封不动地转化为字符串
#define P(func){ \
    printf("%s = %d\n", #func, func); \
}

int main(){
    int a = 7;
    P(MAX(2, 3));
    P(5 + MAX(2, 3));
    P(MAX(2, MAX(3, 4)));
    P(MAX(2, 3 > 4 ? 3 : 4));
    P(MAX(a++, 6));
    P(a);
  //  printf("MAX(2, 3) = %d\n", MAX(2, 3));
  //  printf("5 + MAX(2, 3) = %d\n", 5 + MAX(2, 3));
  //  printf("MAX(2, MAX(3, 4)) = %d\n", MAX(2, MAX(3, 4)));
  //  printf("MAX(2, 3 > 4 ? 3 : 4) = %d\n", MAX(2, 3 > 4 ? 3 : 4));
  //  printf("MAX(a++, 6) = % d\ta = %d\n", MAX(a++, 6), a);

    return 0;
}

实现一个打印LOG函数,输出所在函数及行号等信息。

注:

__FILE__ //以字符串形式返回所在文件名称
__func__ //以字符串形式返回所在函数名称
__LINE__ //以整数形式返回代码行号

可以在编译时使用:gcc xxx.c -D DEBUG 加上一个宏定义

#include<stdio.h>

#define DEBUG //也可以直接定义
#ifdef DEBUG
//变参宏
//##:连接。将两个部分作为一个整体取看待,就允许args为空
#define LOG(frm, args...){ \
    printf("[%s : %s : %d] ", __FILE__, __func__, __LINE__); \
    printf(frm, ##args); \
    printf("\n"); \
}
#else
#define LOG(frm, args...){}
#endif

int main(){
    int a = 123, b = -34;
    char str[100] = "Hello world!";
    printf("[%s : %s : %d] a = %d, b = %d\n",__FILE__, __func__, __LINE__, a, b);
    LOG("a = %d, b = %d\n", a, b);
    LOG("str = %s\n", str);
    LOG("hello world");

    return 0;
}

七、复杂结构和指针

1.结构体(类型)

一个结构体所占用的内存为其字段的总字节数,而结构体中存在对齐方式,操作系统在内存上给相关单元申请空间时,有一个默认的对齐方式,先扫描这个结构里面的基本字段类型,会取占用字节最大的类型的字节数进行对齐

struct node1{ //占用8字节
	char a; //先申请4字节,自用1字节
	char b; //不申请,直接用a剩下的字节。已用2,剩2
	int c; //剩下的2字节不够,申请4字节
}
struct node2{ //占用12字节
	char a; //申请4,用1
	int c; //不够,申请4
	char b; //不够,申请4
}
#include<stdio.h>

struct node1{
    char a;
    char b;
    int c;
} a;
struct node2{
    char a;
    int c;
    char b;
} b;

struct node3{
    char a;
    char b;
    int c;
} c[10];

int main(){

    //在c语言中一定要保留strcut关键字
    printf("sizeof(node1) = %lu\n", sizeof(struct node1)); //8
    printf("sizeof(node2) = %lu\n", sizeof(b)); //12
    printf("sizeof(c) = %lu\n", sizeof(c)); //80

    return 0;
}

2.共用体(类型)

//共用体的内部字段共同利用一片存储单元
//根据字段所占空间最大的申请内存
union register{ 
	struct{ //匿名结构体,仅能使用一次(bytes初始化了一次)
		unsigned char byte1;
		unsigned char byte2;
		unsigned char btye3;
	} bytes; //3字节
	unsigned int number; //4字节 ---> register就占用4字节
}

使用共用体,实现ip转整数的功能【0~255,256个数字,2^8 = 256,即8位,1字节即可,但注意是无符号型,因为符号位也要用来存值】

请添加图片描述
请添加图片描述

大端机:字节的低位—>高地址位

小端机:字节的低位—>低地址位(会造成地址不连续的问题)

#include<stdio.h>

union IP{
    struct {
        unsigned char a;
        unsigned char b;
        unsigned char c;
        unsigned char d;
    } ip;
    unsigned int num;
};

int main(){
    char str[100] = {0};
    int arr[4];
    union IP ip;
    while(~scanf("%s", str)){
        sscanf(str, "%d.%d.%d.%d", &arr[0], &arr[1], &arr[2], &arr[3]);
        //原本是0、1、2、3,因为小端机,可以再倒过来3、2、1、0,解决地址不连续的问题 
        ip.ip.a = arr[3];
        ip.ip.b = arr[2];
        ip.ip.c = arr[1];
        ip.ip.d = arr[0];
        printf("%u\n", ip.num);
    }
    
    return 0;
}

3.变量的地址

操作系统会给内存进行一个连续的编址行为,对应的内存条每一个的字节都会对应成为一个地址,单位是字节。

指针变量就是一种特殊的变量,指针

(32/64位就是说计算机系统是用32/64个bit去表示一个地址)

在32位操作系统中,一个指针变量占4字节

在64位操作系统中,一个指针变量占8字节

用32个2进制位表示一个地址,也就是说它能表示2^32个地址 ==> 4GB
同理,64位操作系统能表示2^64个地址 ==> 理论上是有上限的,实际上是无上限的(很大)

​ 无论是什么类型的地址,在实际上是可以互相存储的,因为在同一个操作系统中指针变量所占用的内存都相同。但是直接这么做是不合适的,应该进行类型转换。

​ 指针变量的类型主要是支持指针变量的计算(只支持+、-)

int a = 0; //占4个字节
&a; //每次取地址取第一个字节的地址,一个int类型的变量有4个地址,一个字节占一个地址。
int *p = &a; //在定义指针变量时,*是一个标志,表示是一个指针变量。p本身也有地址,&a传值时只取了它的第一个字节的地址
int t = *p; //*是一个取值符号。t = 0
p + 1; //向后跳跃了四个字节,每次+1是跳跃一个类型的字节数

int **q = &p; //二级指针变量,指向指针的指针。只要p本身的地址不改变,q的值也不会改变。

*p = 5; //即使p中的值,p中存的是a的地址,所以a = 5

4.等价形式转换

*p == a //a是原始变量
p + 1 == *p[1]
p->filed == (*p).filed == a.filed
#include<stdio.h>

#define P(func){\
    printf("%s = %d\n", #func, func);\
}

struct Data{
    int x, y;
};

int main(){
    struct Data a[2], *p = a;
    //用尽可能多的形式表示a[1].x
    a[0].x = 0, a[0].y = 1;
    a[1].x = 2, a[1].y = 3;
    
    P(a[1].x);
    P((a + 1)->x);
    P((&a[1])->x);
    P(p[1].x);
    P((p + 1)->x);
    P((&p[1])->x);
    P((&a[0] + 1)->x);
    P((&p[0] + 1)->x);
    P((p + 1)->x);
    P((*(&a[1])).x);
    P((*(&p[1])).x);
    P((*(a + 1)).x);
    P((*(p + 1)).x);
    
    P(*((int *)p + 2)); //将p所指向的类型指向整型,p + 1就是向后跳跃4个字节

    return 0;
}

5.typedef的用法

//内建类型的重命名
typedef long long lint; //lint就是long long
typedef char *pchar;
//结构体类型的重命名
typedef struct __node{ 
	int x, y;
} Node, *PNode; //Node n就是struct __node n,*PNode p就是strcut __node *p
//函数指针命名
typedef int(*func)(int); //将当前的func提升为类型
int (*add)(int, int); //变量
typedef int (*add)(int, int); //类型

6.函数指针

Main函数参数(只有操作系统可以调用主函数)

int main();
int main(int argc, char *argv[]); //argc: 命令行参数个数;*argv[]: 二维的字符数组,存储若个行字符串
int main(int argc, char *argc[], char **env); //**env: 二维字符数组,环境变量
#include<stdio.h>

int main(int argc, char *argv[], char **env){
    printf("argc = %d\n", argc);
    for(int i = 0; i < argc; i++){
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    for(int i = 0; env[i]; i++){
        printf("env[%d] = %s\n", i, env[i]);
    }

    return 0;
}

八、工程项目开发

1.函数声明与函数定义的区别

函数未声明:暴露在编译期【g++ -c xx.cpp : 只完成编译期的工作】

函数未定义:暴露在链接期【g++ xx.o:链接编译期生成的文件】

//41.function.cpp
#include <stdio.h>

void funcA(int); //声明
void funcB(int);

//定义
void funcA(int n){
    if(n == 0) return ;
    printf("funcA: %d\n", n);
    funcB(n - 1);
    return ;
}

int main(){
    funcA(5);

    return 0;
}

xxx.c —> C语言源文件

xxx.cpp / xxx.cc —>C++源文件

//41.unite.cc
#include <stdio.h>
void funcA(int n);
void funcB(int n){
    if(n == 0) return ;
    printf("funcB: %d\n", n);
    funcA(n - 1);
    return ;
}

使用:

​ g++ -c 41.function.cpp -->41.function.o

​ g++ -c unite.cc --> 41.unite.o

多文件链接编译:

请添加图片描述

2.头文件与源文件

头文件:xxx.h

源文件:xxx.c / xxx.cpp / xxx.cc

#include <head.h> //从系统库查找
#include "head.h" //从当前目录下开始一层层进行查找

头文件里不可以既放声明又放定义。

规范:头文件里放声明,源文件里放定义。

请添加图片描述

也可以使用:
//-I :将目录映射到库路径下
#include <head.h> //gcc -I./ -c xx.c

3.实现简版的printf函数

首先要创建目录:

src —— 标准的工程项目开发的名字,里面存放源文件

include —— 存放头文件

bin —— 存放a.out

lib —— 存放链接库的库文件
请添加图片描述
请添加图片描述

makefile 【脚本语言(文本脚本/工具)类似于 markdown文档】:方便工程项目开发的多文件链接编译过程

创建名为makefile的文件【touch makefile / touch MAKEFILE】(每次修改文件就重新编译相关的文件):

.PHONY: clean #相当于在一个虚拟路径下创建了一个命令clean,即使当前目录下有名为clean的文件或目录,也不影响
all: main.o ./src/print.o #最后一步需要依赖的文件
        gcc main.o ./src/print.o -o ./bin/a.out #最后一部执行的语句(将所有对象文件链接在一起生成可执行文件)
main.o: main.c ./include/print.h #确定main.o的来源
        gcc -c main.c -I./include #生成main.o
./src/print.o: ./src/print.c ./include/print.h
        gcc -c ./src/print.c -I./include -o ./src/print.o #如果不加上-o后面的内容,就是生成在了和makefile相同的目录下
run:  #直接执行可执行程序
        ./bin/a.out
clean: #清空
        rm main.o
        rm ./src/print.o
        rm ./bin/a.out

请添加图片描述

简版printf函数实现:

(1)include/print.h

#ifndef _PRINT_H
#define _PRINT_H

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

#endif

(2)src/print.c

#include <stdio.h>
#include <stdarg.h>

#define PUTC(a) putchar(a), ++cnt;

int output(char *num, int n){
    int cnt = 0;
    for(int i = n - 1; i >= 0; i--){
        PUTC(num[i]);
    }
    return cnt;
}

int put_d(int n){
    char num[20] = {0};
    unsigned int x; //极小值比极大值多1,int直接存存不下
    int i = 0, cnt = 0;
    if(n < 0) { 
        x = -n, PUTC('-');
    }
    else x = n;

    do {
        num[i++] = x % 10 + '0'; //取整型的最后一位,然后转换成字符
        x /= 10;
    } while(x);
    return cnt += output(num, i);
}

    do {
        num[i++] = x % 10 + '0'; //取整型的最后一位,然后转换成字符
        x /= 10;
    } while(x);
    return cnt += output(num, i);
}

int put_s(char *str){
    int cnt = 0;
    for(int i = 0; str[i]; i++){
        PUTC(str[i]);
    }
    return cnt;
}

int print(const char *format, ...){
    int cnt = 0;
    va_list arg;
    va_start(arg, format);
    for(int i = 0; format[i]; i++){
        //PUTC(format[i]);
        switch(format[i]){
            case '%':{
                switch(format[++i]){
                    case '%': PUTC(format[i]); break;
                    case 'd': cnt += put_d(va_arg(arg, int)); break;
                    case 's': cnt += put_s(va_arg(arg, char *)); break;
                }
            }
            break;
            default: PUTC(format[i]);
        }
    }
    va_end(arg);
    return cnt;
}

#undef PUCT

(3)main.c

#include<stdio.h>
#include <inttypes.h>
#include <print.h>

int main(){
    int a = 123;
    printf("length = %d\n", printf("Hello world\t"));
    print("length = %d\n",print("Hello world\t"));
    printf("length = %d\n", printf("a = %d\t", a));
    print("length = %d\n", print("a = %d\t", a));
    printf("length = %d\n", printf("int(0) = %d\t", 0));
    print("length = %d\n", print("int(0) = %d\t", 0));
    printf("length = %d\n", printf("int(1000) = %d\t", 1000));
    print("length = %d\n", print("int(1000) = %d\t", 1000));
    printf("length = %d\n", printf("int(-123) = %d\t", -123));
    print("length = %d\n", print("int(-123) = %d\t", -123)); 
    printf("length = %d\n", printf("int(100500) = %d\t", 100500));
    print("length = %d\n", print("int(100500) = %d\t", 100500));

    printf("length = %d\n", printf("INT32_MAX = %d\t", INT32_MAX));
    print("length = %d\n", print("INT32_MAX = %d\t", INT32_MAX));
    printf("length = %d\n", printf("INT32_MIN = %d\t", INT32_MIN));
    print("length = %d\n", print("INT32_MIN = %d\t", INT32_MIN));

    char str[100] = "I love China!";
    printf("length = %d\n", printf("str = %s\t", str));
    print("length = %d\n", print("str = %s\t", str));
    printf("length = %d\n", printf("str = %s\t", "Hello World!"));
    print("length = %d\n", print("str = %s\t", "Hello World!"));

    return 0;
}

链接库

①动态链接库【xxx.so】

②静态链接库(重点)【xxx.a】

要生成的静态链接库的名字规范:libxxx.a【前后缀必须是lib和.a】

请添加图片描述

要是对您有帮助,点个赞再走吧~ 欢迎评论区讨论~

  • 21
    点赞
  • 160
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
C语言是一种广泛应用于软件开发的编程语言,具有简洁、高效、灵活等特点。为了更好地学习和掌握C语言,整理一份完整的笔记是非常有必要的。 首先,C语言的基础知识是理解和掌握该语言的关键。笔记中应包含C语言的基本语法,如变量的声明和定义、数据类型、运算符、控制语句等,这些是编写C程序的基础。 其次,C语言的函数也是非常重要的一部分。函数是C语言中的一种模块化的编程方式,可以提高代码的重用性和可维护性。在笔记中应包含函数的定义和调用、参数传递、函数返回值等内容,以及常用的C库函数的使用。 此外,C语言的数组和指针也是需要重点关注的内容。数组是一种存储多个相同类型数据的方式,而指针则是C语言中与内存地址相关的操作。在笔记中应包含数组的定义和初始化、多维数组的使用、指针的声明和运算等内容。 还有,C语言中的内存管理是需要特别注意的。动态内存分配是C语言的一个重要特性,可以根据需要在运行时分配和释放内存。笔记中应包括动态内存分配的函数,如malloc、free等,以及内存泄漏和内存溢出的问题的解决方法。 最后,C语言的文件操作也是需要掌握的一部分。文件操作是C语言中与磁盘文件进行读写的关键部分,可以实现数据的长久保存和共享。在笔记中应涵盖文件的打开和关闭、读取和写入、文件指针的操作等内容。 综上所述,C语言笔记的完整版应该包含C语言的基础知识、函数、数组和指针、内存管理以及文件操作等内容。通过阅读和整理笔记,可以更好地理解和应用C语言,提高程序编写的效率和质量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值