标签: C语言

C语言笔记分享

  • 从本质理解指针的那些知识 | C语言基础

    从本质理解指针的那些知识 | C语言基础

    指针在C语言中是个很重要的概念,我更多从类比并结合计算机硬件的角度开始引入概念。

    我也是初学者,指针也确实是个很容易被误解的概念,请多指教,评论区留言误区一般时间1h内会回。

    1. 引入指针

    1.1 简要理解

    在之前的《谈谈printf()和scanf() | C语言基础》文章中,讲到变量的时候说过,变量每次创建同时会在内存中分配其一个空间。

    我们从高中时期或者更早就了解过计算机硬件的简单知识,如下导图(=>完整版指路)。

    硬件基础思维导图

    我们知道计算机上CPU(Central Processing Unit 中央处理器)在处理数据的时候,会从内存中读取数据,处理后的数据也会放回内存中。内存中为了高效管理空间,会把内存分为一个个的内存单元,一单元一字节,且每个单元都有自己的既定的地址。

    更简单地,可以把内存想象成为一栋按房间编号管理的宿舍楼,然后每个“房间”就容纳一个字节,如果寝室内发生什么事儿,门牌号就是宿管找到你们寝室的地址。

    所以类比下,当CPU需要一段数据时候,必须要有个准确的门牌号,否则就无从找到目标。C语言中这个门牌号就叫做“指针”,指针你可以看作地址的另一个叫法。

    1.2 拓展:从硬件角度理解 【想了解的可以点此展开】

    一般理解到这里就差不多了,不过也许有人觉得这样解释地还是过于简单,那从再从硬件角度来看指针是如何起作用的(博主大一还没学计算机组成之类的课,仅为自主课外拓展和理解)。

    硬件角度图解参考

    当我们写下 int* pa = &a; 并执行时,(假设 a 是前文已经定义过的变量,基础知识回顾:&是取地址操作符,故 &a 即代表 a 变量存储在内存中的地址),编译器会把 pa (指针本身)的数值写入CPU的MAR(寄存器)中。

    随后地址总线将这一串二进制位送往内存控制器,内存条内部的译码电路接收到地址后找到确切的内存单元。接着数据总线把地址 &a 存储到MDR,再将其返回寄存器或缓存。

    部分概念再类比上述例子,按执行顺序排列:

    • Memory Address Register (MAR):CPU 内部的寄存器,用来暂存即将访问的内存地址;相当于把“门牌号”写在便签上准备递给内存系统。一般为单向的(CPU → 内存/IO)。
    • 地址总线 (Address Bus):把 MAR 中的地址位输出到主板/芯片上的地址导线上,送往内存控制器。如同走廊/道路网络,负责把门牌号传递到内存。(现代主流芯片物理地址线 39–52 位,但方便说明)假设其线的条数=CPU的字长(64位或32位,32位有时也表达为x86),比如32位机器下常见有32根地址总线产生的2进制序列作为一个地址,一个地址有32位,常需要4个字节。(32bit=4Byte)存储。
    • 行列译码器:含译码电路,在内存芯片内部决定“第几层楼、第几号房”,锁定内存单元。
    • 数据总线 (Data Bus):把目标单元中的数据从内存传回 CPU,也可以在写操作时把数据送入内存。是种双向的总线(读时内存 → CPU,写时 CPU → 内存)。
    • Memory Data Register (MDR):又叫 Memory Buffer Register (MBR),也是一种CPU内部的寄存器,负责在 CPU 与内存之间暂存刚读到的数据或待写出的数据;可将其看作各个房间之间投递的快递员的手中的包裹。
    • 缓存(Cache):热数据暂存地,加快访问(若命中缓存,即可直接MAR → MDR),但不影响“地址=定位信息”的本质。

    从本质上讲,指针只是一串能够唯一定位某个内存单元的地址数字(比如x64下Debug时形如0x000000CD3450F6E4的十六进制数)。它本身不保存数据,而是告诉程序“你要的数据在哪里”

    2. 指针变量

    2.1 基本格式和大小

    在 C 语言里,我们仍然需要一个变量来存放这串地址数字,这样的变量我们称之为“指针变量”。

    #include <stdio.h>
    
    int main()
    {
        int a = 10;
        int* pa = &a;
        printf("%p", pa);
    }

    如上,声明时候的* 号在说明 pa 即为指针变量,指针变量内保存的是 a 变量的值的地址,然后通过 printf() 来打印出来。

    如下,a 变量赋值为10,&a定位到地址发现已经被存入了内存中(00 00 00 0a,逆序,这里的a是代表十六进制表达下的10)。也就是 pa 并不直接持有 10 整数的值,而是保存了 a 所在内存单元的地址。
    VS中内存窗口

    你可能会好奇,那指针变量是存的地址,每个地址的大小在机器里就已经固定好了的,那么指针变量大小和地址大小一样,为什么还要在声明指针变量的时候去写上类型呢?

    指针变量声明时的类型图解

    原因在于:当我们沿着地址取数时,必须事先知道打算读取多少字节。int*
    “告诉”编译器,一步跨四格;char* “告诉”它,一次只走一格。这样既能保证数据对齐,也能避免读写越界。

    后文 2.2指针操作中的“± 整数值”部分也有讲解。

    一次走几格,我们来看看指针变量的各种操作。

    2.2 指针操作/应用

    解引用操作符 *

    又称间接寻址运算符。这个主要是为了解决找到地址指向的对象。你看我们存地址也是有需要取出来用的时候。C语言中利用解引用操作符 * ,来通过已知指针变量找到其指向的变量。

    看下面的案例,注意只有在指针变量名前面加的 * 才算是解引用操作符。int* 中的 * 不是。

    #include <stdio.h>
    int main()
    {
        int a = 10;
        int* pa = &a;
        *pa = 0;
        printf("%d", a);
    }

    如上这个很简单的案例,其中*pa 就是通过 pa 指针变量中存放的地址,找到指向的 a 变量。而且可以利用此方法间接修改了a 变量的值,此代码执行后输出0。

    这样的间接修改的方式,相比直接 a = 0; 来说,对变量的调用等操作更加灵活。

    注意:在 C 里进行解引用必须满足以下前提——

    • 指针非空(不为 NULL);
    • 指针指向的地址处于程序已分配的有效内存范围;
    • 解引用类型与目标对象匹配,以免因字节对齐或大小差异造成未定义行为。

    ± 整数值

    这个主要是为了解决上文2.1末说的“一步跨几格”的问题。看下示意图 ptr-2 的部分更易理解:
    加减整数值效果图解

    #include <stdio.h>
    int main()
    {
        // [模拟Debug逐行执行过程]
        int ages[4] = { 0, 1, 2, 3 };  
        // 创建数组ages: { 0, 1, 2, 3 }
        int* ptr = &ages[2];  
        // ptr是ages数组第三个元素的指针变量
        *ptr = 8;  
        // 修改第三个元素为8 => 数组ages: { 0, 1, 8, 3 }
        ptr++;
        // ptr是ages数组第四个元素的指针变量
        *(ptr - 2) = 9;
        // 修改第二个元素为9 => 数组ages: { 0, 9, 8, 3 }
    }

    按上面这个案例,因为数组元素在内存中是连续存放的。指针往前移动一步两步是直接由加减后面的整数来得知的,非常显然。

    但是如果从其十六进制地址数字来看,地址的数字又增加了多少呢?

    #include <stdio.h>
    int main()
    {
        int n = 10;
        int* pi = &n;  
        // 原先int类型的地址赋给指针变量pi
        char* pc = (char*)&n; 
        // 强转int类型的地址为char类型的地址,赋给指针变量pc
    
        // 自C99标准起,把指针强转为 void* 才是完全符合标准
        // 往右滑,有对齐的便于观察的输出结果
        printf("  &n   = %p\n", (void*)&n);     // Output:   &n   = 00AFF974
        printf("  pi   = %p\n", (void*)pi);     // Output:   pi   = 00AFF974
        printf("pi + 1 = %p\n", (void*)pi + 1); // Output: pi + 1 = 00AFF978
        printf("  pc   = %p\n", (void*)pc);     // Output:   pc   = 00AFF974
        printf("pc + 1 = %p\n", (void*)pc + 1); // Output: pc + 1 = 00AFF975
    }

    我们看到原来的 int* 声明的指针变量 pi 的十六进制地址 ,在 pi + 1+4sizeof(int)),这就是“一步跨四格”;原来的 char* 声明的指针变量 pc,在 pc + 1+1(sizeof(char)),这就是“一步跨一格”。

    指针 – 指针

    实际应用案例比如在数组中可以表示元素个数差,也可以在数组中限定要遍历的范围。(如下)

    在算法场景中也有用于“快慢指针”算法的长度统计等等。

    #include <stdio.h>
    
    int main()
    {
        int nums[5] = {10, 20, 30, 40, 50};
        int *left  = &nums[1];   // 左指针
        int *right = &nums[4];   // 右指针
    
        // gap元素个数差
        int gap = right - left; 
        printf("gap = %d\n", gap); 
        //Output: gap = 3
    
        // 用左右指针遍历区间 [left, right) 
        for (int *p = left; p != right; ++p)
        {
            printf("%d ", *p);      
        }              // Output:20 30 40
        puts("");      // 换行
    }

    注意:两个指针必须指向同一个连续对象(通常是同一数组,或者刚好其一指针指向结尾位置)才能安全相减,否则跨数组或随意相减会导致未定义行为。

    函数内实现两个值的改变

    比如说写一个交换函数 swap()

    #include <stdio.h>
    void swap(int*, int*);
    int main()
    {
        int a=3, b=5;
        swap(&a, &b);
        printf("%d, %d\n", a, b);
    }
    void swap(int *p1, int *p2)
    {
        int t;
        t = *p1;
        *p1 = *p2;
        *p2 = t;
    }

    实际上,这样的值传递,地址没有改变,存放的变量的值改变了。

    总结下要使某个变量的值通过函数调用发生改变:

    1. 在主调函数中,把该变量的地址作为实参
    2. 在被调函数中,用形参(指针)接受地址
    3. 在被调函数中,改变形参(指针)所指向变量的值

    3* 写在最后:

    Q:为什么我还是继续在更C语言内容的原因?

    A:首先我们学校的进度偏慢吧,而且我自己也对这部分的知识点的学习发散得也多。(我后续的学习计划会加入Java,静待更新吧~)

    由此就会有很多的知识堆积在脑海中,我希望通过费曼的方式把这些知识用公众号(博客)文章的形式进行结构化输出。

    所以其实我不是一个技术大牛,也是个不断学习者。如果你有发现什么错误,欢迎留言!我将速速赶到然后与你交流技术话题。

  • 从猜数字程序来理解结构和随机数 | C语言基础

    从猜数字程序来理解结构和随机数 | C语言基础

    由于博主过去学过Python,本篇文章中会有很多对照的分析。
    本次为了更利于各部分的理解,我们从一个简单的猜数字程序的实现来说起…

    1. 程序实现流程图:

    程序实现流程图

    2. 随机数生成

    Python中生成随机数的方式,可以利用内置的 random 模块进行生成。而且每次文件运行时输出的随机数都是不同的,同一文件内若多次调用随机数函数也都是不同的。

    如下就是生成并打印1~100以内的整数的Python程序代码:

    import random
    randomNum = random.randint(1,100)
    print(randomNum)

    对于C语言中,如果要实现随机数,会稍显复杂。

    首先,需要 rand() 函数实现伪随机数,为什么说是“伪”呢?是因为每次运行文件内的多个 rand() 函数时候之后的几次都和第一次是一个值。

    伪随机数示范

    说明这样的“随机”有可能是经过某种算法得到的,而不是真的随机。调查后,事实上 rand() 函数每次都是对一个叫“种子”的基准值运算后生成的。那么我们就会想“种子”是如何得出的?根据什么来算出的?

    根据此,可以想到用 srand() 函数来初始化随机数的生成器,通过 srand() 函数的 seed 参数来设置需要 rand() 函数生成随机数的“种子”。也就是说 srand() 的“种子”如果是随机的, rand() 就能生成随机数,但是生成随机数的前提是还需要一个随机的值,这样的矛盾又该如何解决呢?我们需要找到一个时刻在变的值来依据此得出这个需要的随机值。

    那么我们可以使用程序运行的时间来作为“种子”,那么我们可以再引入 time() 函数。 time() 函数返回的类型是 time_t 类型(本质上还是32位或64位的整型)的时间戳值。时间戳是1970年1月1日0时0分0秒到程序运行时候的时间差,单位是秒。time() 函数中的参数 timerNULL 即可返回差值。time_t 类型对于 srand() 函数是不接受的,所以强制类型转化 unsigned int

    综上(其实上面的你没听懂也没关系),我们可以写出生成随机数的方式了。

    注意需要新引入三个库:stdio.hstdlib.htime.h

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(){
        srand((unsigned int)time(NULL));
        printf("%d\n", rand());
    }

    那么为了猜数字的范围缩小为1~100,我们可以对rand()函数结果对100取模。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(){
        srand((unsigned int)time(NULL));
        printf("%d\n", rand() % 100 +1);
    }

    3. 分支结构

    3.1 Intro:关系操作符+逻辑操作符

    • 关系操作符
      • >(大于)
      • >=(大于等于)
      • <(小于)
      • <=(小于等于)
      • == (等于):特别注意这里是两个等号,在很多计算机语言中都是双等号表示“等于”(或是相同、相等)的判定符号
      • != (不等于)
    • 逻辑操作符
      • && (与):两边均为true才输出true,有一个false结果一定是false。(记忆方式:数字电路的方框形逻辑门中间的符号就是& )
        与逻辑示意图
      • || (或):两边其中一个为true就输出true,两边均为false才输出false。(记忆方式:数字电路的方框形逻辑门中间大于等于号下面是两条线的)
        或逻辑示意图

    这些运算符的结果与算术运算符算出实际的结果不同,而是返回1(true)和0(false)整型值,以此来控制我们的程序接下来选择Y(Yes符合条件)的分支还是N(No不符合条件)的分支。

    而且这些操作符都是双目的,也就是符号两边都需要有值。

    注意与Python的符号表达不同的一点是,Python里 A<x<Bx>A and x<B 是实现的一个效果,都是把x值限定在(A,B)区间内,而C语言中对应的方式最好是 x>A && x<B,另一种形式( A<x<B )虽然不会报错,但对于C语言程序来说,执行时会有误解,实际上执行的是 (A<x)<B ,那么就是先判断A和x,然后才是把前面判断的结果和B再做判断,通常会达不到想要的结果。

    3.2 if-else

    #include <stdio.h>
    int main()
    {
        int age = 0;
        scanf("%d", &age);
        if (age >= 18) {
            printf("Adult\n");
        } else {
            printf("Child\n");
        }
    }

    如上是个很简单的年龄判断的 if-else 分支结构,结构也类似于Python里的。如果输入的age值是大于等于18就输出Adult,其他情况(也就是 age 值小于18)就输出Child。

    那么还可以用嵌套的结构,能实现如果两个if条件+其他的三种分支。

    #include <stdio.h>
    int main()
    {
        int score = 0;
        scanf("%d", &score);
        if (score >= 90) {
            printf("Great!\n");
        } else if (score >= 60) {
            printf("Pass\n");
        } else {
            printf("Failed\n");
        }
    }

    这里的 else if 就是类似于Python中的 elif ,这里其实可以看作是两个 if-else ,第一个结构中的 else 部分嵌套入了第二个结构。

    如上,我们可以简单写出猜数字程序中的用分支结构表示的比大小的部分。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand()%100+1;    // 随机数范围 1~100
        int num = 0;                   // 用户输入的猜测数
        printf("请输入 1-100 之间的数字:");
        scanf("%d", &num);
        if (randomNum > num) {
            printf("猜小了\n");
        }
        else if (randomNum == num) {
            printf("猜对啦\n");
        }
        else {
            printf("猜大了\n");
        }
        return 0;
    }

    需要提一下的是,C语言的 scanf() 虽然在功能上基本和Python中的 input() ,但是在输入的是字符串的情况下,不支持读取带空格的字符串。

    3.3 条件操作符

    还有种方式可以表达这样的分支结构,不过更多地是用于双分支或者三分支。但是三分支的实现如果要说代码的可读性上考虑,不太推荐使用。

    基础的双分支的条件操作符的结构为:

    逻辑或关系表达式 ? 真值表达式 : 假值表达式

    就是那么简洁,一个问号和冒号就连接了三个表达式。来看极简的两数求最大的案例:

    int max = (a > b) ? a : b;

    那么我们上面的判断部分的如果要写成三分支(不推荐,为了清晰起见按行分层缩进了):

    printf("%s\n",
           (randomNum > num)   ? "猜小了":
           (randomNum == num)  ? "猜对啦":
                               "猜大了");

    双分支:

    if (randomNum == num){
        printf("猜对啦");
    } else {
        printf("%s\n", (randomNum > num) ? "猜小了" : "猜大了");
    }

    4. 循环结构

    猜数字游戏中我们还希望做回合数( round )的限制,如果是猜错(即输出“猜大了”“猜小了”这两种)情况下,可以消耗回合数再猜一次。如果可猜的回合耗尽,则输出“GameOver”结束游戏。

    4.1 while

    C语言中非零即为true,零直接是false,可以借此作为循环次数的控制条件。设定好了我们总回合数,那么每执行完一次判断则可以直接减去一次回合数。当回合数为0正好可以结束循环条件。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand() % 100 + 1;   // 随机数范围 1~100
        int num = 0;                      // 用户输入的猜测数
        int round = 5;                    // 最大回合数
    
        // while 循环控制回合,从 round 到 1
        while (round) {
            printf("请输入 1-100 之间的数字:");
            scanf("%d", &num);
            if (num == randomNum) {
                printf("猜对啦\n");
                break;
            } else if (num < randomNum) {
                printf("猜小了\n");
            } else {
                printf("猜大了\n");
            }
            round-=1;
        }
        if (round == 0) {
            printf("GameOver\n");
        }
        return 0;
    }

    不过,while循环一般更多用于未知循环次数的情况时,比如在刷题的时候经常会看到不知道会输入多少数代表输入终止(EOF)。

    ▼ 牛客网OJ上的初始代码

    #include <stdio.h>
    int main() {
    &nbsp; &nbsp; int a, b;
    &nbsp; &nbsp; while (scanf("%d %d", &a, &b) != EOF) { // 注意 while 处理多个 case
    &nbsp; &nbsp; &nbsp; &nbsp; // 64 位输出请用 printf("%lld") to
    &nbsp; &nbsp; &nbsp; &nbsp; printf("%d\n", a + b);
    &nbsp; &nbsp; }
    &nbsp; &nbsp; return 0;
    }

    4.2 for

    for 循环就是常用于我们的已知循环次数的程序里了。

    我们直接来看两个最常用的Python和C语言同样实现迭代的代码,这样看是不是好理解了。

    for count in range(1, round + 1):
        print("这是第", count, "次循环")

    Python中使用 range() 生成的整数序列作为可迭代对象,这个整数序列的区间是 [1, round] 末尾不取,函数默认的步长是1,实现效果就是把count变量的值不断从1取到round。

    C语言中的对于起始值、终止条件、步长各部分用分号断开,这基本的三要素还是非常明显的。

    for (int count = 1; count <= round; count++){
        printfprintf("这是第 %d 次循环\n", count);
    }

    那么我们猜数字程序用 for 循环就能这样写:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand() % 100 + 1;   // 随机数范围 1~100
        int num = 0;                      // 用户输入的猜测数
        int round = 5;                    // 最大回合数
    
        // for 循环控制回合,从 1 到 round
        for (int count = 1; count <= round; count++) {
            printf("第 %d 轮,请输入 1-100 之间的数字:", count);
            scanf("%d", &num);
            if (num == randomNum) {
                printf("猜对啦\n");
                break;
            } else if (num < randomNum) {
                printf("猜小了\n");
            } else {
                printf("猜大了\n");
            }
            if (count == round) {
                printf("GameOver\n");
            }
        }
        return 0;
    }

    5. 补充的分支与循环结构

    C语言中还有些别的分支和循环结构,接下来会一一道来。

    5.1 switch 分支

    switch 语句像一个分流路口:给它一个整数(或枚举、字符),它就把程序送到对应的 case 代码块,比长串 if‑else if 好读得多。

    写法发生效果
    switch (表达式)先算出表达式的值,再去找相同数字的 case
    case 常量:对上号就从这里开始执行。
    default:前面都不对时走这里,可省略。
    break;结束 switch,跳到大括号后面的第一条语句。
    如果不写的话,

    break 和 continue

    • break:立刻结束当前最近的一层 forwhiledo‑while 循环或 switch 的条件分支,程序跳到循环后面的第一条语句。
    • continue:立刻结束本次迭代,把控制权送回循环头;for 会先执行增量表达式再检查条件,while/do‑while 会直接检查条件。在不套循环的 switch 里敲 continue,编译器会直接报错。
    • 在嵌套循环中,它们只作用于最内层。

    只打印 1‑10 中的奇数,遇到 7 就提前停止:

    #include <stdio.h>
    
    int main() {
        for (int n = 1; n <= 10; ++n) {
            if (n == 7)
                break;          // 终止整个循环
            if (n % 2 == 0)
                continue;       // 跳过偶数,继续下一轮
            printf("%d ", n);
        }
        puts("循环结束");
        return 0;
    }
    
    //Output:
    //1 3 5 
    //循环结束
    • continue 让偶数分支直接回到 for 的自增步骤。
    • break 在遇到 7 时立即离开循环,剩余数字不再检查。

    这样,你就能直观看到两条语句在循环里的“停一停”( continue )与“走人”( break )效果。

    5.2 do‑while 循环

    do‑while 像一次先行动、后条件判断的循环:不管条件真假,主体最少执行一次

    写法发生效果
    do { ... } while (条件);先跑 do { ... },再判断 while (条件);条件为真就回到 do 继续,为假就结束循环。

    例如,累加直到用户输入 0:

    #include <stdio.h>
    
    int main() {
        int n, sum = 0;
        do {
            printf("输入数字(0 结束): ");
            scanf("%d", &n);
            sum += n;
        } while (n != 0);
        printf("总和为 %d", sum);
        return 0;
    }

    PS:这并非是一定要使用do-while循环才能实现。

    5.3 goto 不定转移

    goto 是一句直接跳转:给它一个标签名,程序立刻冲过去执行标签下面的代码。

    写法会发生效果
    <label>:普通语句前加标签名,供 goto 跳入。
    goto <label>;立即跳到同函数内的 label: 处。

    比如下面这个分数输入的代码,遇到输入检查失败就跳到清理位置:

    #include <stdio.h>
    
    int main(void) {
        int score;
        printf("请输入分数(0~100): ");
        if (scanf("%d", &score) != 1)
            goto ERROR;
        if (score < 0 || score > 100)
            goto ERROR;
        printf("成绩合法,分数是 %d", score);
        return 0;
    
    ERROR:                        
        printf("输入非法,程序结束。");
        return 1;
    }

    goto 语句如果使⽤的不当,就会导致在函数内部随意乱跳转,打乱程序的执⾏流程,所以我们的建议是能不⽤尽量不去使⽤;更多情况推荐 breakreturn 或把逻辑拆成函数更易读。

  • 谈谈printf()和scanf() | C语言基础

    谈谈printf()和scanf() | C语言基础

    不同于Python,直接就可以使用类似的print()input(),C语言中这两个库函数都需要引入最基础的表示输入输出的stdio.h头文件
    #include <stdio.h>

    博主环境使用Windows 11,C语言IDE大多用的是Visual Studio 2022,校内使用CFree5;
    按认识顺序撰文,从helloworld认识printf(),而后认识scanf()

    1. printf() 打印输出

    相比于博主之前学过的Python语言中的print(),C语言中的printf(),从函数名上多了个f字母,代表format(格式),说明可以定制输出⽂本的格式,这文本格式也叫数据类型。

    数据类型

    Intro

    参考文档:https://zh.cppreference.com/w/c/language/arithmetic_types#.E6.95.B0.E6.8D.AE.E6.A8.A1.E5.9E.8B

    sizeof() 函数

    sizeof()是个用于计算操作符类型长度的函数,输出的单位为字节(Bite)(换算:1Bite=8bit)
    虽然输出的单位是字节,看着输出类型好像是整数,但是根据实际的编辑器输出的附加提示可得这是size_t类型。所以printf中的占位符应该是%zd

    #include <stdio.h>
    int main()
    {
        printf("%zd",sizeof(int));  // 输出:4(表示int类型的长度为四个字节)
        return 0;
    }
    根据头文件来查数据类型对应的取值范围
    • limits.h ⽂件中说明了整型类型的取值范围。
    • float.h 这个头⽂件中说明浮点型类型的取值范围。(有的编译器下可能没有写)
      取值范围的极限值都是定义在头文件中的一些常量里的,比如INT_MININT_MAX代表int数据类型的最⼩值和最⼤值,这些我们叫宏常量
    // 节选自limits.h
    
    #define INT_MIN &nbsp; &nbsp; (-2147483647 - 1)
    #define INT_MAX &nbsp; &nbsp; &nbsp; 2147483647

    integer 整型

    • short:长度为2,范围为 -32767~32767
    • int:长度为4,范围为 -2147483647~2147483647
      (初学者目前刷到的题目都是int就能解决的,只要注意题目中有关于2147483647的提示就直接用int,巧记Tips:我们学校的学号正好和此范围都是十位数)
    • long:长度为4,范围为 -2147483647L~2147483647L (注意末尾有L)
    • long long:长度为8,范围为 -9223372036854775807i64~9223372036854775807i64
      (在算法竞赛中,为了不让长度作为判题有误的限制,一般使用long long
    • unsigned int: 范围为0~4294967295,可以简写为unsigned
    关于 signed 或 unsigned

    C语⾔使⽤ signedunsigned 关键字修饰字符型和整型的。
    signed 关键字,表⽰该类型带有正负号,可以包含负值;
    unsigned 关键字,表⽰该类型不带有正负号,只能表⽰零和正整数。
    默认不带此关键字的话,省略的前缀为 signed
    整数变量声明为 unsigned 的好处是,同样⻓度的内存能够表⽰的最⼤整数值,增⼤了⼀倍

    floating-point 浮点型

    ps:到浮点型的话,范围数字很长,所以这里直接用十进制小数点位数来代表范围

    • float:长度为4,单精度,精确到小数点后6位,范围的末尾有f
    • double:长度为8,双精度,精确到小数点后15位。
    • long double:长度为8(msvs)或16(gcc),精确到小数点后15位,但实际范围比double更大。

    character 字符型

    char 字符类型,长度为1,字符型的常量一般用单引号包围'',这点尤其注意。
    如果是转义字符,比如 \n\0\t都算作一个字符。
    再比如有些看不见的操作也可以算作一个字符,比如空格和换行。

    bool 布尔型

    C语⾔原来并没有为布尔值单独设置⼀个类型,⽽是使⽤整数 0 表⽰false(假),⾮零值表示true(真)
    在 C99 标准中引⼊了布尔类型来表⽰真假的,需要包含<stdbool.h>头文件 #include <stdio.h>
    (对于老一点的编译器可能需要C99支持,比如CFree)
    布尔类型(_Bool)长度为1,其类型的变量取值仅有truefalse两个。

    #include <stdio.h>
    int main()
    {
        // 整型
        printf("%hd", 5); //short对应的
        printf("%d", 5);  // int对应的【常用】
        printf("%ld", 5);  // long对应的
        printf("%lld", 5);  // long long对应的
    
        // 浮点型:根据小数点后面取到的位数不同选择不同的数据类型
        printf("%f", 3.14f);  // float对应的,注意末尾有f
        printf("%lf", 3.1415); // double对应的 【常用】
        printf("%Lf", 3.1415); // long double 对应的,注意大写L
    
        // 字符型:字符和其对应的ASCII码值是对应的
        printf("%c", 'A');
        printf("%c", 65);
    
        // 字符串
        printf("%s", "HelloWorld");
    }

    输出格式

    基本格式

    printf() 不会在⾏尾⾃动添加换⾏符,运⾏结束后,光标就停留在输出结束的地⽅,不会⾃动换 ⾏
    那么其中有占位符表示输入格式的部分,需要借助双引号参数1外的参数加入这个位置。

    占位符到转换说明

    占位符就是写在第一个字符串参数中%开头的,形如%d%f,在一定位置可表示要填入的参数格式/控制参数输出格式的这类字符。
    占位符表格:

    数据类型变量声明类型占位符
    短整型short%hd
    整型int%d
    无符号整型unsigned int%u
    长整型long%ld
    无符号长整型unsigned long int%lu
    长长整型long long%lld
    单精度浮点型float%f
    双精度浮点型double%lf
    长双精度浮点型long double%Lf
    字符型char%c
    字符串型String%s
    指针(独立类型%p

    如果对于上面表格中写的部分,那么只看这种占位符其实也是转换说明,但如果加上下面这些后就算是转换说明,转换说明基本格式如同%m.pX 或 %-m.pX

    限定最小宽度

    在占位符中间插入数字(或转化说明基本格式中的小数点前的数字m)即代表输出的最小宽度。
    最小宽度的理解:宽度对于整型来说就是数字位数,如果说位数小于最小宽度,会在数字前面自动补空格直到是最小宽度;如果给出的位数已经超过了最小宽度,那么直接输出。
    默认在数字前面补空格即右对齐,如果需要左对齐的话,可以再在%后加一个-

    #include <stdio.h>
    int main(){
        printf("%d\n", 123);   // Output:"123"
        printf("%5d\n", 123);  // Output:"  123"
        printf("%-5d\n", 123); // Output: "123  "
        return 0;
    }
    始终显示正负号

    在占位符中间插入+就可以了。(和上面的的-的用法要分清哦!)

    #include <stdio.h>
    int main()
    {
        printf("%+d\n", 123);  // Output: "+123"
        printf("%+d\n", -123); // Output: "-123"
        return 0;
    }
    限定小数点后位数

    %和字母之间的小数点后紧接着的数字(或转化说明基本格式中的小数点前的数字p)就代表要保留的位数,会四舍五入到这个指定的位数。

    #include <stdio.h>
    int main()
    {
        printf("%.2f\n", 1.233);  // Output: "1.23"
        printf("%.2f\n", 1.236);  // Output: "1.24"
        return 0;
    }

    ps:默认不指定保留几位的时候,输出的最多位数取决于数据类型精度。
    可以和限定最小宽度的一起使用,小数点算一个宽度

    #include <stdio.h>
    int main()
    {
        printf("%5.2f\n", 1.236);  // Output: " 1.24"
        return 0;
    }
    输出部分字符串

    如果把上面限定小数点后位数的方法,用到字符串输出用的%s上,那就是可以指定字符串输出的长度了。

    #include <stdio.h>
    int main()
    {
        printf("%.7s\n", "Nibbles is handsome.");  // Output: "Nibbles"
        return 0;
    }
    限定值以 * 代
    #include <stdio.h>
    int main()
    {
        printf("%*.*f\n", 6, 2, 0.5);  // Output: "  0.50"
        return 0;
    }

    2. scanf() 输入

    除了按照上述的直接输出的程序方式。一个完整的简单程序应该有IO两部分(Process处理后续涉及):Input(输入),Output(输出)。 输出方式就是上面的 printf() 打印到屏幕,输入就是scanf()
    先给示例:

    #include <stdio.h>
    int main()
    {
        int num = 0;
        scanf("%d", &num);
        printf("Number: ", num);
    }

    变量

    定义 = 声明 + 内存分配

    int num = 0;

    这样是定义了一个变量 num,并将其初始化,初始值设置为0。
    变量的定义过程是 int num 部分,此时会在内存中分配一个空间。

    extern 单独声明

    extern int external_var;

    单独只有声明的需要使用 extern 关键字,一般在头文件中声明,但不分配内存。
    但是如果在其他文件中已经有定义了变量,在该文件中想要引用其他文件的定义可以使用 extern

    变量的生命周期控制

    全局变量

    在函数的 { } 之外定义的变量,初始化如果不指定是默认为0。
    如果是在定义之前使用 extern (如上在头文件中单独声明案例)
    在内存的静态存储区。在整个程序中:程序开始执行时创建,在程序结束时销毁。

    局部变量

    在函数的 { } 之内定义的变量,要是出了花括号的部分就没法再引用。
    在整个程序中:进入程序中变量所在函数时创建,在退出该函数时销毁。
    通常存储在栈区(auto 变量)。使用 static 关键字把变量变为静态变量,默认初始值才可以是0,否则没有初始值是随机数,此时在内存的静态存储区。

    变量的命名规则

    • 可以包含的字符:字母(a-z, A-Z)、数字(0-9)和下划线(_) 。
    • 第一个字符必须是字母或下划线。
    • 变量名区分大小写。
    • 变量名不能是C语言的关键字。
      • 关键字:auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
    • 应选择具有描述性的变量名,以便于理解变量的用途(不建议学大学里课堂上那样,为了图快而使用abc这样的单字母变量名)。

    输入格式

    基本格式

    如下在程序执行时,会在冒号后面停留光标,等待你输入整数。之后回车即可把刚输入的整数赋给value整型变量。

    第一个参数就是格式化字符串,之后的参数的基本格式应该是 <取地址运算符><变量名>
    一次可以读入多个变量,但是要注意,有多少个占位符/转换说明,后面其后就要有多少个参数,遵循按顺序对应。如果一次输入多个值,各个值之间常可以用空格换行隔开,函数处理时候会⾃动过滤空⽩字符,包括空格、制表符、换⾏符。(但是如果是%c会把空格作为一个字符,如果要忽略可以写成% c,即在%和字母间加个空格)
    scanf()函数中的占位符决定了输入的格式。如果输入的格式不一样的话,会强制把输入的截断到与占位符表示的格式一样的部分。

    #include <stdio.h>
    int main()
    {
        int num_int;
        float num_float;
            // Input: "3.14"
        scanf("%d", &num_int);  
        printf("%d\n", num_int);   // Output: "3"
        scanf("%f", &num_float);
        printf("%f\n", num_float); // Output: "0.140000"
    }

    还可以简化为:

    #include <stdio.h>
    int main()
    {
        int num_int;
        float num_float;
            // Input: "3.14"
        scanf("%d%f", &num_int, &num_float);  
        printf("INT: %d\nFLOAT: %f\n", num_int, num_float); 
        /**
        Output:
            INT: 3
            FLOAT: 0.140000
        */
    }

    字符串输入不建议使用该函数,%s 不会包含空⽩字符,所以⽆法⽤来读取多个单词组成的短语/句子。读入时不会检测字符串是否超过数组⻓度。储存字符串时,很可能报错表示超过数组的边界。

    函数返回值和EOF文件结束

    在刷题时候,碰到题目要求函数式编程,在题目中已经给出了基本的代码结构,如下:

    #include <stdio.h>
    void print(int m, int n);
    
    int main() {
        int m, n;
        while (scanf("%d%d", &m, &n) != EOF) {
            print(m,n);
            printf("\n");
        }
        return 0;
    }

    在while的表达式里有关于scanf()函数返回值的条件语句。
    scanf()函数的返回值是个整数,且是和函数的占位符数是一致的。如果什么都没读取到就是返回0。如果在成功读取任何数据之前,发⽣了读取错误或者遇到读取到⽂件结尾,则返回常量 EOF (-1)。
    那么这个EOF的结束效果如何在我们输入中体现呢?
    Ctrl+Z 连按三次(VS2022中的效果),或者其他有的编译器中按一次即可。

    赋值忽略说明

    这一符号的设计主要是服务用户的。比如你规定了日期的输入格式为2025/4/19,结果输了个2025.4.19。此时就需要赋值忽略符(assignment suppression character)这样的处理了。

    #include <stdio.h>
    int main()
    {
        int year = 0;
        int month = 0;
        int day = 0;
        scanf("%d%*c%d%*c%d", &year, &month, &day);
        printf("%d-%d-%d", year, month, day);
        return 0;
    }

    这里的 %*c 中间的*就是作为赋值忽略符,这里的 %*c 可以不必和后面参数对应,不必返回,且读取到值也不输出,我们输出格式在 printf()函数里标定了 <year>-<month>-<day>

  • C语言初入门 —— 基础的杂记

    C语言初入门 —— 基础的杂记

    当你开始学其他语言,回头看Python。你会对Python之禅中“简单优于复杂”有所体会,Python太简洁了😂

    我目前是暑假自学状态,所以本笔记是融合了国际知名的《Harvard CS50x》和国内b站的《鹏哥C语言》。和以前学的Python有些结构重合的部分我就不再提了,文中有的会有些许疑问,我还在学习,若有记得不对的可以提出来哈~

    CS50x:写程序更关注于自己的想法,以及实现需要的函数、条件、循环、变量。

    C语言学习笔记

    //头文件:引入标准输入输出库stdio是缩写,不要拼成studio!
    #include <stdio.h>

    C语言中的变量要先声明变量类型

    main函数是程序的入口 ,C语言程序是从main函数的第一行开始执行。且一个项目中只能有一个main(),一个项目不代表一个文件。

    printf("填入占位符\n", 占位符要代表的)
     //%c,%f,%s,%d这类是占位符。当然也可以直接用。
    

    语言的char单字符数字类型,应当用单引号括住单字符,双引号可以用于字符串(或零字符的字符串)

    字符的本质就是ASCII编码。

    printf("%c\n",'a');
    printf("%c\n",97);//两者输出结果相同
    

    C语言字符串中个特殊的知识,就是在字符串的末尾隐藏放着一这个\0字符是字符串的结束标志。 所以打印函数或者计算字符串长度strlen()的函数会在遇到此字符后停止打印或计算。 观察方法:F10+调试-窗口-监视,填入字符串并展开即可看到。 没有\0字符的情况有可能使用了字符数组

    char arr[] = {'a','b','c','d','e','f'};
    printf("%s\n",arr);
    //但是输出就会没有终止,往往打印的时候后面会尾随乱码。为了避免这种情况,在字符数组后要自己手动添加\0
    char arr[] = {'a','b','c','d','e','f','\0'};

    strlen()string.h的库函数(需要include),统计的就是\0前的字符个数(不计\0本身) 函数本身返回的是size_t类型的,所以占位符最好换成%zd

    C语言中的switch也是代表选择结构的关键字之一

    C语言中还有do while循环

    转向语句:break语句、goto语句、continue语句、return语句

    行内注释用//,多行的要用/* */

    PythonC
    elifelse if
    or||
    and&&
    i+=1 / i-=1可用左边的形式,
    但若每次都加1则可表达为i++ / i–

    C语言的各种数据类型都有自己的长度,计算操作符数用sizeof(),其返回C语言规定是只能是无符号整数(0和正整数),为了统一各系统返回值类型,定义了 size_t 类型来表示,故没有规定具体的类型:unsigned intunsigned long long等。括号内如果放的是表达式,则不会真实地计算。

    signed和unsigned:sign能表示正负数,unsign即无符号

    char不确定是否有无符号,取决于编译器的实现。大部分的情况char == unsigned char

    int默认是signed,且打印的占位符为%d。(unsigned打印占位符为%u)

    数据类型——模具 ,C语言中变量需要声明数据类型,若有给初始值的过程即初始化。无初始化的话不能让你用。已有初始化后又给值那就叫赋值。

    float num = 3.14f;

    全局变量在花括号外,局部变量在花括号内,局部值优先。

    除号 / 两端都是整数,即为整数除法(且结果也为整数-只有整数部分丢弃小数部分)

    负数取模的规则是,结果的正负号由第一个运算数的正负号决定

    连续赋值,从右往左赋

    单目操作符(++,–等),之前讲的符号中两边都要跟东西的是双目操作符

    环境配置

    《鹏哥C语言》中是用VS2022来教学的,还有关于scanf()函数的配置问题,我写到Gitee Wiki中了。

    but,我要强推CS50x课程对教学用IDE的解决方案!
    (国外的讲师深知教学应该更重于idea和实现的代码,而非前期的繁杂的环境配置)

    课程中使用了远程VSCode作为编辑器,并且云端已经全部搭建好C语言的环境了,开箱即用!
    这样的CodeSpace可以通过Github登录授权后拥有:https://cs50.dev
    (至于速度的话,你都能流畅在YouTube上看课了,同样网络环境下打开这链接,速度还是快的)

    对于编译的操作还简化了很多,编译一个hello.c只需下面两条命令

    make hello
    ./hello

    (后面课程Arrays中介绍了make实际上是clang的自动执行脚本,笔记)

    同时侧边栏安装了一个图标是一个爱quack叫的吉祥物鸭子的插件,点开一看,是个免费的AIChat!
    不过,它的回答只能是英文的哦~ 基本上是能看懂滴

    如下,这只鸭子的回答真的很喜欢quack🦆,也有可以在线使用的地方:https://cs50.ai/chat

    还有安装Docker,也许后面的课程还会教项目的上线吧。