跳转到内容

C语言笔记

大杂烩,一些学习笔记,看书摘抄、总结等,觉得有用、有复习温习价值的就记录下来。

C和C++标准库

标准库相关文档资料:

比较有权威参考性的网址:

标准库的历史:

随着语言的发展新的papers(指官方的叫标准的书)会被发布,每一次都定义一个新的标准。这就是为什么我们会有不同的C和C++版本的原因:C99, C11, C++03, C++11, C++14等等,数字与出版/发布年份相符。

这些标准都市非常详细和有技术新的文档:我不会把它们当作手册。通常会分为两部分:

  • C/C++的功能和特性;
  • C/C++的API--开发人员可以用于他们的C/C++程序的一个类、函数和宏的集合。它也被称为标准库。

C标准库也称为ISO C库,是用于完成诸如输入/输出处理、字符串处理、内存管理、数学计算和许多其他操作系统服务等任务的宏、类型和函数的集合。它是在C标准中(例如C11标准)中定义的。

举例,以c标准库历史介绍:

  • ANSI C共包括15个头文件。
  • 1995年,Normative Addendum 1 (NA1)批准了3个头文件(iso646.h、wchar.h和wctype.h)增加到C标准函数库中。
  • C99标准增加了6个头文件(complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h)。
  • C11标准中又新增了5个头文件(stdalign.h、stdatomic.h、stdnoreturn.h、threads.h和uchar.h)。

至此,C标准函数库共有29个头文件。

我们从这里开始讨论真正的代码了。从事于标准库实现的开发者阅读官方的ISO规范并将其转化为代码。他们必须依赖其操作系统所提供的功能(读/写文件,分配内存,创建线程,......所有这些被称为系统调用),因此每个平台都有其自己的标准库实现。有时它是系统内核的一部分,有时它是作为一个附加组件 - 编译器 - 必须单独下载。

和C标准库的概念类似,但仅针对C ++。C++标准库是一组C++模板类,它提供了通用的编程数据结构和函数,如链表、堆、数组、算法、迭代器和任何其他你可以想到的C++组件。C ++标准库也包含了C标准库,并在C++标准中进行了定义(例如C++ 11标准)。

GNU C库,也称为glibc, 是C标准库的GNU项目实现。并非所有的标准C函数都可以在glibc中找到:大多数数学函数实际上是在libm库中实现的,这是一个独立的库。截至今天,glibc是Linux上使用最广泛的C库。然而,在90年代期间,有一段时间里,glibc有一个竞争对手称为Linux libc(或者简称libc),它是由glibc 1.x的一个分支产生的。在一段时间里,Linux libc是许多Linux发行版中的标准C库。

经过多年的发展,glibc竟然比Linux libc更具优势,并且所有使用它的Linux发行版都切换回了glibc。所以,如果你在你的磁盘中找到一个名为libc.so.6的文件,请不要担心:它是现代版的glibc。

为了避免与之前的Linux libc版本混淆,版本号增加到了6(他们无法将其命名为glibc.so.6:所有Linux库都必须以lib前缀打头)。

在Linux系统下,C语言标准库通常位于以下几个目录中:

  • /usr/include:这个目录包含了C标准库的头文件(即.h文件),这些头文件定义了库函数的声明和宏定义。
  • /usr/lib:这个目录包含了C标准库的编译好的对象文件(即.a文件,静态库)和共享对象文件(即.so文件,动态库)。静态库在程序编译时链接,而动态库在程序运行时链接。
  • /usr/lib64:在64位Linux系统上,一些库可能位于这个目录,特别是64位版本的库。

例如,标准输入输出库stdio.h的头文件通常位于/usr/include/stdio.h,而对应的库文件可能位于/usr/lib/libc.so(动态库或/usr/lib/libc.a(静态库)。要查找特定库文件的位置,可以使用find或locate命令。例如,要查找标准C库libc的共享对象文件。

静态库在程序编译时链接,而动态库在程序运行时链接。

linuxmint上,c标准库路径:/usr/lib/x86_64-linux-gnu/libc.*

包括静态库和动态库。

libc.a libc.so libc.so.6

也可以通过命令查出本机的标准库位置:

ldconfig -p | grep libc.so

在大多数Linux系统中,libc.so.6是默认的C库。

查看标准库版本:

另一方面,C++标准库的实现位于libstdc++或GNU标准C++库中。这是一个正在进行的在GNU/Linux上实现标准C++库的项目。一般来说,所有常规的Linux发行版都默认使用libstdc++。

在Mac和iOS上,C标准库的实现是libSystem的一部分,libSystem是位于/usr/lib/libSystem.dylib中的核心库。LibSystem包含其他组件,如数学库、线程库和其他底层实用程序。关于C++标准库,在OS X Mavericks(V10.9)之前的Mac上,libstdc++是默认选项。这在现代的基于Linux的系统上可以找到的同样的实现。自OS X Mavericks开始,Apple切换到使用libc++,这是LLVM项目,替代了GNU libstdc++标准库。IOS开发者可以使用iOS SDK(软件开发工具包)来访问标准库,它是一系列允许创建移动应用程序的工具。

在Windows上,标准库的实现一直严格限定在Visual Studio中,它是微软官方的编译器。

他们通常称之为C/C++运行时库(CRT),并且它涵盖了c/c++二者的实现。

Bionic是Google为其Android操作系统所编写的C标准库实现,它直接在底层使用。

第三方开发者可以通过Android原生开发工具包(NDK)访问Bionic,该工具集允许你使用C和C++代码编写Android应用程序。

在 C++ 端, NDK提供了很多版本的实现:

  • libc++,从Lollipop开始的官方安卓系统和现代Mac操作系统都将其作为C++标准库使用。从NDK发布17版本开始,它将成为NDK中唯一可用的C++标准库实现;
  • gnustl,libstdc++的别名,这两者在GNU/linux是同一个库。这个库的已被弃用,它将在NDK发布18中删除;
  • STLport,由STLport项目编写的C++标准库的第三方实现,自2008年以来一直处于不活跃状态。与gnustl一样,STLport将在NDK发布18中移除。

查看gcc支持的语言版本

查看方式:gcc后面输入 -std=c版本或者c++版本

报错表示明确不支持,没有报错表示支持;具体支持的版本详情,要用man进去查看更具体的说明。

汇编与汇编编译器

开源汇编编译器:

NASM 与 YASM、FASM,这三个都是免费开源的汇编编译器,总体上都是采用的Intel的语法。

yasm是从nasm的基础上开发出来的,属于同宗,使用了相同的语法,所以nasm的代码可以用yasm编译。

yasm虽然更新较慢,但对nasm一些不合理的地方进行了改良。从这个角度来看,yasm比nasm更优秀些,而nasm更新快,能支持更新的指令集,而且还支持Gas语法和AMD64(EM64T)架构,跨平台,支持多种目标文件格式。

在Windows平台上,fasm是另一个不错的选择,平台支持比较好,可以直接用来开发Windows上的程序,语法也比较独特,是自己专属语法,它非常简洁,但也可能需要用户适应,特别是如果他们之前使用过NASM或MASM。在对Windows程序结构的支持上,fasm是3个免费的编译器里做得最好的。

  • FASM:FASM的社区相对较小,但非常活跃。文档可能不如NASM或YASM丰富,但仍然足够帮助用户开始使用。
  • NASM:NASM有一个庞大的社区和广泛的文档资源,这个是首选的关键因素。
  • YASM:YASM的社区虽然不如NASM大,但也相当活跃,并且受益于与NASM的兼容性。

学习上的选择。语法的原因,排除了fasm。

masm只支持windows,排除。除非是特别好的书,而且配套工具只有windows环境。

gas使用at&t风格,不是intel风格,比较古董,不是业界主流,排除;机器上只有gcc时,可以用来查看gcc生成的汇编,但不用来学习和实际编程。

学习和实际编程,选择nasm和yasm。二者首选nasm。学习还是以nasm的教材为主

NASM:

官网:https://www.nasm.us/

使用文档:

ubuntu/linux mint 安装直接用apt,这种一般不是最新版,但也够用了。

bash
sudo apt install nasm
nasm -v

C代码转成intel风格的汇编代码:

bash
gcc -S -masm=intel example.c

YASM:

官网:http://yasm.tortall.net/

使用文档:http://yasm.tortall.net/Guide.html

ubuntu/linux mint 安装直接用apt,这种一般不是最新版,但也够用了。

bash
sudo apt install yasm
yasm --version

NASM与其他比较:

GAS即GNU AS汇编编译器,其属于AT&T风格,我们常用的GNU的产品还有GCC/G++。生成.s文件。

NASM是Linux平台下常用的汇编编译器,是intel风格的汇编编译器,生成.asm文件。

MASM是Windows平台下的汇编编译器,也使用Intel风格,我们学8086汇编时使用的就是MASM编译器。

debug是16位的调试工具,目前基本上没有用。而且它缺少宏等功能,写汇编语言基本上活受罪,现在除了学校里面学习,没人用了。

nasm是跨平台的开源工具,在Windows平台下面不太好用。Unix/Linux平台下面倒是很有些用户群了。相对简洁可读性比较高,代码看起来比较优美。

MASM可以编写Windows程序,支持宏,一直随着Visual Studio的更新而更新,并不单独发布了。支持最新的64位操作系统,用起来也比较方便,但是学习资料很少。它主要用来对VC++写的程序进行局部调优。VC中的__asm { }指令,一般我们都用它来写汇编语言,进行VC的局部调优。

上面三个,都是基于Intel语法的,还有一个gas汇编器,是AT&T语法的,不太简洁。但最好是能看懂,linux内核代码和一些人会使用gas/att风格。

其他参考:

clang-format与C代码风格

clang-format:

官网文档:

各个配置中文解释:

在线预览配置的网站:https://clang-format-configurator.site

使用clang-format生产配置文件:

bash
clang-format -style=llvm -dump-config > .clang-format

主流C代码风格:

Linux 内核代码风格,可以多读几遍,重点参考:

google的风格:

还有其他的代码风格,llvm等等。

K&R 很重要,代码基本都是大家参考的对象。

K&R 主要风格摘要:

来自k&r 第二版。除了上面所表示的容易出错的风格,其他的花括号,k&r风格总是在if for while等,只要下面最多一个语句块,即(最多一个块,或者一行语句以分号;结尾),后面省略花括号,如果超过1个,那就加上花括号。简洁为主。比如下面的风格:

kernel风格与K&R风格的case处理一致,都是不缩进,并且case后面冒号没有花括号。个人总结,case缩进不缩进都可以。既然无所谓还是与大佬观点保持一致。

K&R的if与else处理:

其他风格比如google和内核风格是不鼓励这种,if和else应该保持一致,都有花括号。个人总结:倾向google和内核的方式。

K&R while有时加花括号,有时不加,没有固定套路。

很多地方都是,有的加有的不加。

个人总结:while里面内容稍微多点的话,加上花括号肯定比不加更容易阅读。

K&R和kernel风格都不推荐一行放多个语句,比如if return等,再怎么短的语句都不会放到一行。google风格会放到一行。个人总结:还是遵从前者,没必要为了代码看起来紧凑,牺牲代码可读的一致性。

自己的代码风格:

同时参考linux kernel和llvm,最终形成自己的配置.clang-format:

yaml
# clang-format官方参数配置说明:
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
# linux kernel代码风格说明:
# https://www.kernel.org/doc/html/v5.9/translations/zh_CN/process/coding-style.html
###### 基于LLVM的默认配置上修改
BasedOnStyle: LLVM
# 最大空行保持
MaxEmptyLinesToKeep: 3
# c++11 c++14 c++17 c++20等
Standard: Latest
###### tab 空格 缩进
# 是否使用tab 需要配合vscode的insertSpaces配置
# llvm的风格:Never linux的风格:Always
UseTab: Never
# 缩进宽度
# llvm默认2空格 linux是tab缩进 默认是8
# 2太短 8太宽 取折中4
IndentWidth: 4
# tab的宽度 llvm和linux风格默认都是设置8
# 改成与缩进宽度一样的4
TabWidth: 4
###### 使用linux风格的配置
# 函数大括号换行
BreakBeforeBraces: Linux
# case是否缩进 linux风格是不缩进
IndentCaseLabels: false
# 允许短的函数放在同一行
# None, InlineOnly(定义在类中), Empty(空函数),
# Inline(定义在类中,空函数), All
#  linux风格是None llvm风格是All
AllowShortFunctionsOnASingleLine: None

指针相关的学习

小哥对指针的讲解很好,应该是全网最好的指针讲解视频。

咖喱味英语,但有中文字幕,学习无碍。

B站视频:

笔记与更多资料:

编译器对c和cpp的识别

编译器针对不同后缀文件,一般是按照以下这样对待:

  • .c文件当成c编译
  • .cpp当成c++编译
  • .c文件用纯c编写,当后缀改成cpp时,可以当成c++编译

具体场景示例: 比如bool,纯c是c99才有的,并且要引入#include <stdbool.h>。但文件名改成cpp后,可以不用加include,因为当成c++编译了,c++有bool。

浮点数的计算机表示

结构体的内存对齐

参考:

结构体struct内存对齐的3大规则:

1.对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类 型的整数倍;

2.结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内 存大小是结构体内最大数据成员的最小整数倍;

3.如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再 考虑当前类型以及最大结构体内类型。

块作用域变量声明

应该摒弃旧有的写法,使用新标准。

消除编译器while赋值告警

strcpy strncpy

strcat strncat

strcmp 字符串比较的规则

长字符串拼接

为什么C指针那么让人困惑和纠结

原因就是:C 语言混乱的语法,以及指针和数组之间奇怪的兼容性。

对于指针和数组的相互关系,市面上多数的 C 语言入门书籍只是含混其辞地做了敷衍解释(包括 K&R,我认为该书是诸恶之源)。这还不算,他们还将本来已经用数组写好的程序,特地用指针运算的方式重新编写,还说什么“这样才像 C 语言的风格”。K&R和对应的经典C已经是完全过时的东西。虽然 K&R 被很多人奉为“神书”,可是对于我来说,它连作为菜鸟实习的资料也不够格。

内存泄漏、悬空指针

对程序而言,不可再访问到的内存块被称作垃圾(garbage)。留有垃圾的程序存在内存泄 漏(memroy leak)现象。

malloc calloc realloc

malloc为字符串分配内存

关于malloc前面需不需要强制转换:

答案:

在C语言中,void 型指针进行强制类型转换是没必要的,因为 void型 的指针会在赋值操作时自动转换为任何指针类型。

对返回值进行强制类型转换的习惯来自经典 C。 在经典 C 中,内存分配函数返回 char *型的值,用强制类型转换是必要的。(使用新标准就完全没必要强制转换了,别用老古董的经典C)

在C中,不一定要使用强制类型转换,但是在C++中必须使用。所以,使用强制类型转换更容易把C程序转换为C++程序。但除此之外似乎没有其他理由这么做了。

动态分配字符串的内存,一般配合strcpy(p, "abc")来初始化。

malloc + memset = calloc

指针创建没有名称的数组

++与*的优先级

指针处理多维数组的行和列

const修饰

C开发工具

CMAKE规则

GCC编译常用命令参数

GCC的优化级别

GCC的警告级别

C语言static

C标准定义变量的位置

还有for循环,int的位置,要尽量使用C99以上的标准,不要用老古董的方式。

指针变量大小

在32位操作系统中, 存储器地址以32位数字的形式保存, 所以它叫32位操作系统。 32位==4字节, 所以 64位操作系统要用8个字节来保存地址。

数组变量与指针的区别

char s[] = "How big is it?";

char *t = s;

sizeof结果不一样。

数组变量不能赋值,指针可以。

指针退化:数组传递给函数,退化成指针,这样就只有数组的地址,没有数组的长度。

sizeof

sizeof是运算符,不是函数;编译器在编译时就计算sizeof,即确定存储空间大小。

scanf与fgets

scanf()允许传递格式字符串, 就像你对printf()函数做的那样,甚至可以用scanf()一次 输入多条信息:

char first_name[20];

char last_name[20];

printf(" Enter first and last name: ");

scanf("%19s %19s", first_name, last_name);

printf("First: %s Last:%s\n", first_name, last_name);

如果不对scanf做限制,会发生缓冲区溢出:

fgets限制输入大小:

二者不同场景的用法:

double类型的printf和scanf

输入:

scanf 对于 float 只能用%f

对于 double 只能用%lf

浮点数底层存储不同。

输出:

printf 的 %f 可输出 float 和 double 类型

事实上: printf 中没有定义 %lf ,但很多系统可能会接受它,要保证可移植性,坚持只使用%f。

C
/*
    读取两个实数值,用实数显示出它们的和、差、积、商
*/

#include <stdio.h>

int main(void)
{
    double vx, vy;      /* 浮点数 */

    puts("请输入两个数。");
    printf("实数vx:");   scanf("%lf", &vx);
    printf("实数vy:");   scanf("%lf", &vy);

    printf("vx + vy = %f\n",  vx + vy);
    printf("vx - vy = %f\n",  vx - vy);
    printf("vx * vy = %f\n",  vx * vy);
    printf("vx / vy = %f\n",  vx / vy);

    return 0;
}

puts与printf对比

C
/*
    显示出读取到的两个整数的和
*/

#include <stdio.h>

int main(void)
{
    int n1, n2;

    puts("请输入两个整数。"); /* puts的s是string的意思 输出后会自动回车
    效果跟printf("...\n")类似 但不可进行格式设置和数值输出 */
    printf("整数1:");   scanf("%d", &n1);
    printf("整数2:");   scanf("%d", &n2);

    printf("它们的和是%d\n", n1 + n2);      /* 显示和*/

    puts("60% is enough for me.");  /* puts没有格式设置,所以%不用转义成%% printf 则需要转义 */
    printf("60%% is enough for me.\n");

    return 0;
}

printf %f默认显示小数点后面6位

C
/*
    整数和浮点数
*/

#include <stdio.h>

int main(void)
{
    int n;          /* 整数 */
    double x;       /* 浮点数 */

    n = 9.99;
    x = 9.99;

    printf("int    型变量n的值:%d\n", n);            /*    9      */
    printf("             n / 2:%d\n", n / 2);       /*    9 / 2  */

    printf("double 型变量x的值:%f\n", x);            /* 9.99       */
    printf("             x/2.0:%f\n", x / 2.0); /* 9.99 / 2.0 */
    // printf %f 默认显示小数点后面6位数字

    return 0;
}
/*
int    型变量n的值:9
             n / 2:4
double 型变量x的值:9.990000
             x/2.0:4.995000
*/

printf宽度与对齐

%5d %5.1f 表示输出值的最少宽度,包含小数点,默认右对齐,不足在左边补空格,超出则显示实际宽度。设置-表示左对齐。

C
/*
    读取三个整数,并显示出它们的合计值和平均值
*/

#include <stdio.h>

int main(void)
{
    int a, b, c;
    int sum;                /* 合计值 */
    double ave;             /* 平均值 */

    puts("请输入三个整数。");
    printf("整数a:");   scanf("%d", &a);
    printf("整数b:");   scanf("%d", &b);
    printf("整数c:");   scanf("%d", &c);

    sum = a + b + c;
    ave = (double)sum / 3;      /* 类型转换 */

    printf("它们的合计值是%5d\n",   sum);     /* 输出99999 */
    printf("它们的平均值是%5.1f\n", ave);     /* 输出999.9 */

    return 0;
}


/*
    格式化整数和浮点数并显示
*/

#include <stdio.h>

int main(void)
{
    printf("[%d]\n",      123);
    printf("[%.4d]\n",    123); //啥意思?
    printf("[%4d]\n",     123);
    printf("[%04d]\n",    123);
    printf("[%-4d]\n\n",  123);

    printf("[%d]\n",      12345);
    printf("[%.3d]\n",    12345);
    printf("[%3d]\n",     12345);
    printf("[%03d]\n",    12345);
    printf("[%-3d]\n\n",  12345);

    printf("[%f]\n",      123.13);
    printf("[%.1f]\n",    123.13);
    printf("[%6.1f]\n\n", 123.13);

    printf("[%f]\n",      123.13);
    printf("[%.1f]\n",    123.13);
    printf("[%4.1f]\n\n", 123.13);

    return 0;
}


/*
    第2章总结
*/

#include <stdio.h>

int main(void)
{
    int a;
    int b;
    double r;   /* 半径 */

    printf("整数a和b的值\n");
    scanf("%d%d", &a, &b); // %d和%d之间不用有间隔,输入时会自动按照空格来分隔数值

    printf("a + b = %d\n", a + b);  /* 加法运算:双目+运算符 */
    printf("a - b = %d\n", a - b);  /* 减法运算:双目-运算符 */
    printf("a * b = %d\n", a * b);  /* 乘法运算:双目*运算符 */
    printf("a / b = %d\n", a / b);  /* 商:/ 运算符 */
    printf("a %% b = %d\n", a % b); /* 余数:% 运算符 */

    printf("(a+b)/2 = %d\n",   (a + b) / 2);
    printf("平均值 = %f\n\n", (double)(a + b) / 2);

    printf("半径\n");
    scanf("%lf", &r);

    printf("半径为%.3f的圆的面积是%.3f\n", r, 3.14 * r * r);

    return 0;
}

字符数组、字符串的初始化

switch - case - break

C
/*
    确认switch 语句动作的程序
*/

#include <stdio.h>

int main(void)
{
    int sw;

    printf("整数:");
    scanf("%d", &sw);

    switch (sw) {
     case 1  : puts("A");  puts("B");  break;
     case 2  : puts("C");
     case 5  : puts("D");  break;
     case 6  :
     case 7  : puts("E");  break;
     default : puts("F");  break;
    }

    return 0;
}

显示单一字符常量putchar

C
/*
    输入一个整数,连续显示出该整数个*
*/

#include <stdio.h>

int main(void)
{
    int no;

    printf("正整数");
    scanf("%d", &no);

    while (no-- > 0)
        putchar('*');
    putchar('\n');

    return 0;
}

do-while

C
/*
    逆向显示输入的正整数
*/

#include <stdio.h>

/*--- 返回输入的正整数 ---*/
int scan_pint(void)
{
    int tmp;

    do {
        printf("请输入一个正整数:");
        scanf("%d", &tmp);
        if (tmp <= 0)
            puts("\a请不要输入非正整数。");
    } while (tmp <= 0);
    return tmp;
}

/*--- 返回正整数倒转后的值 ---*/
int rev_int(int num)
{
    int tmp = 0;

    if (num > 0) {
        do {
            tmp = tmp * 10 + num % 10;
            num /= 10;
        } while (num > 0);
    }
    return tmp;
}

int main(void)
{
    int nx = scan_pint();

    printf("该整数倒转后的值是%d\n", rev_int(nx));

    return 0;
}

全局变量或静态变量自动初始

C
/*
    确认拥有静态存储期的对象的默认的初始化
*/

#include <stdio.h>

int fx;                     /* 用0初始化 */

int main(void)
{
    int i;
    static int    si;       /* 用0初始化 */
    static double sd;       /* 用0.0初始化 */
    static int    sa[5];    /* 所有元素都用0初始化 */

    printf("fx = %d\n", fx);
    printf("si = %d\n", si);
    printf("sd = %d\n", sd);

    for (i = 0; i < 5; i++)
        printf("sa[%d] = %d\n", i, sa[i]);

    return 0;
}

%d %u %lu

C
/*
    显示字符型和整型数据类型的表示范围
*/

#include <limits.h>
#include <stdio.h>

int main(void) {
    puts("该环境下各字符型、整型数值的范围");
    printf("char           : %d~%d\n", CHAR_MIN, CHAR_MAX);
    printf("signed char    : %d~%d\n", SCHAR_MIN, SCHAR_MAX);
    printf("unsignd char   : %d~%d\n", 0, UCHAR_MAX);

    printf("short          : %d~%d\n", SHRT_MIN, SHRT_MAX);
    printf("int            : %d~%d\n", INT_MIN, INT_MAX);
    printf("long           : %ld~%ld\n", LONG_MIN, LONG_MAX);

    printf("unsigned short : %u~%u\n", 0, USHRT_MAX);
    printf("unsigned       : %u~%u\n", 0, UINT_MAX);
    printf("unsigned long  : %lu~%lu\n", 0, ULONG_MAX);

    return 0;
}

字符数组传参

字符串是字符数组反之不一定

数组形参,本质是指针(数组的首地址)。

数组与指针实现实现字符串

C
/*
    用数组实现的字符串和用指针实现的字符串
*/

#include <stdio.h>

int main(void) {
    char str[] = "ABC"; /* 用数组实现的字符串 */
    char *ptr = "123";  /* 用指针实现的字符串 */

    printf("str = \"%s\"\n", str); /* str是指向第一个字符的指针 */
    printf("ptr = \"%s\"\n", ptr); /* ptr是指向第一个字符的指针 */

    return 0;
}

指针初始化的无法更新,数组实现的可以更新。

本质的存储方式:

数组初始化的则不是常量区,不是只读。实现方式是复制一份字符串字面值的副本(保存在栈中)。

初始化习惯,使用习惯,告诉编译器只读、不可修改。

字符串数组和指针数组

C
#include <stdio.h>

int main(void) {
    int i;
    char a[][5] = {"LISP", "C", "Ada"};
    char *p[] = {"PAUL", "X", "MAC"};

    for (i = 0; i < 3; i++)
        printf("a[%d] = \"%s\"\n", i, a[i]);

    for (i = 0; i < 3; i++)
        printf("p[%d] = \"%s\"\n", i, p[i]);

    return 0;
}

指针数组:

指针实现字符串复制

C
/*
    复制字符串
*/

#include <stdio.h>

/*--- 将字符串s复制到d ---*/
char *str_copy(char *d, const char *s) {
    while (*d++ = *s++)
        ;
    return d;
}

int main(void) {
    char str[128] = "ABC";
    char tmp[128];

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

    printf("复制的是:%s", tmp);
    scanf("%s", tmp);

    str_copy(str, tmp);

    puts("复制了。");
    printf("str = \"%s\"\n", str);

    return 0;
}

str_copy的另一种写法

C
char *str_copy(char *d, const char *s) {
    while (*d++ = *s++)
        ;
    return d;
}

另外一种场景的错误使用:

C
/*
    复制字符串(误例)
*/

#include <stdio.h>

/*--- 将字符串s复制到d ---*/
char *str_copy(char *d, const char *s) {
    char *t = d;

    while (*d++ = *s++)
        ;
    return t;
}

int main(void) {
    char *ptr = "1234";
    char tmp[128];

    printf("ptr = \"%s\"\n", ptr);

    printf("复制的是:%s", tmp);
    scanf("%s", tmp);

    str_copy(ptr, tmp); /* 将tmp复制到ptr */

    puts("复制了。");
    printf("ptr = \"%s\"\n", ptr); /* 显示复制后的ptr */

    return 0;
}

C语言的字符串库 string.h

  • strlen
  • strcpy(s1,s2) 将s2复制到s1
  • strncpy(s1,s2,n) 将s2n个字符复制到s1
  • strcat(s1,s2) s2拼接到s1后面
  • strncat(s1,s2,n)
  • strcmp(s1,s2) s1>s2返回正数,小於则返回负数
  • strncmp(s1,s2,n)

字符串转换函数来源于库stdlib.h

  • atoi 字符串转为int
  • aitl 字符串转为long
  • atof 字符串转为double

结构体

uct animal 与enum animal 一样是,类型的意思。

指针(地址)可以使用this。

结构体与typedef

typedef 与c++中的引用类似,都是别名的意思。可以简化struct animal这种写法。(语法糖,看起来像类。)

typedef的时候结构名如student可以省略,然后只使用别名。

结构体可以作为函数返回值

C
/*
    返回结构体的函数
*/

#include <stdio.h>

/*=== xyz结构体 ===*/
struct xyz {
    int x;
    long y;
    double z;
};

/*--- 返回具有{x,y,z}的值的结构体xyz ---*/
struct xyz xyz_of(int x, long y, double z) {
    struct xyz temp;

    temp.x = x;
    temp.y = y;
    temp.z = z;
    return temp;
}

int main(void) {
    struct xyz s = {0, 0, 0};

    s = xyz_of(12, 7654321, 35.689);

    printf("xyz.x = %d\n", s.x);
    printf("xyz.y = %ld\n", s.y);
    printf("xyz.z = %f\n", s.z);

    return 0;
}

结构体的使用示例1:

C
/*
    汽车行驶
*/

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

#define sqr(n) ((n) * (n))

/*=== 表示点的座标的结构体 ===*/
typedef struct {
    double x; /* X座标 */
    double y; /* Y座标 */
} Point;

/*=== 表示汽车的结构体 ===*/
typedef struct {
    Point pt;    /* 当前位置 */
    double fuel; /* 剩余燃料 */
} Car;

/*--- 返回点pa和点pb之间的距离---*/
double distance_of(Point pa, Point pb) { return sqrt(sqr(pa.x - pb.x) + sqr(pa.y - pb.y)); }

/*--- 显示汽车的当前位置和剩余燃料 ---*/
void put_info(Car c) {
    printf("当前位置:(%.2f, %.2f)\n", c.pt.x, c.pt.y);
    printf("剩余燃料:%.2f\n", c.fuel);
}

/*--- 使c指向的汽车向目标座标dest行驶 ---*/
int move(Car *c, Point dest) {
    double d = distance_of(c->pt, dest); /* 行驶距离 */
    if (d > c->fuel)                     /* 行驶距离超过了燃料 */
        return 0;                        /* 无法行驶 */
    c->pt = dest;                        /* 更新当前位置(向dest移动) */
    c->fuel -= d;                        /* 更新燃料(减去行驶距离d所消耗的燃料) */
    return 1;                            /* 成功行驶 */
}

int main(void) {
    Car mycar = {{0.0, 0.0}, 90.0};

    while (1) {
        int select;
        Point dest; /* 目的地的座标 */

        put_info(mycar); /* 显示当前位置和剩余燃料 */

        printf("开动汽车吗【Yes···1 / No···0】:");
        scanf("%d", &select);
        if (select != 1)
            break;

        printf("目的地的X座标:");
        scanf("%lf", &dest.x);
        printf("        Y座标:");
        scanf("%lf", &dest.y);

        if (!move(&mycar, dest))
            puts("\a燃料不足无法行驶。");
    }

    return 0;
}

结构体的使用示例2:

C
/*
    表示日期的结构体和表示人的结构体
*/

#include <stdio.h>

#define NAME_LEN 128 /* 姓名的字符数 */

/*=== 表示日期的结构体 ===*/
struct Date {
    int y; /* 年 */
    int m; /* 月 */
    int d; /* 日 */
};

/*=== 表示人的结构体 ===*/
typedef struct {
    char name[NAME_LEN];  /* 姓名 */
    struct Date birthday; /* 生日 */
} Human;

/*--- 显示指针h所指向的人的姓名和生日 ---*/
void print_Human(const Human *h) {
    printf("%s(%04d%02d%02d日生)\n", h->name, h->birthday.y, h->birthday.m, h->birthday.d);
}

int main(void) {
    int i;
    struct Date today; /* 今天的日期 */

    Human member[] = {
        {"古贺政男", {1904, 11, 18}},
        {"柴田望洋", {1963, 11, 18}},
        {"冈田准一", {1980, 11, 18}},
    };

    printf("请输入今天的日期。\n");
    printf("年:");
    scanf("%d", &today.y);
    printf("月:");
    scanf("%d", &today.m);
    printf("日:");
    scanf("%d", &today.d);

    printf("今天是%d%d%d日。\n", today.y, today.m, today.d);

    printf("--- 会员一览表 ---\n");
    for (i = 0; i < sizeof(member) / sizeof(member[0]); i++)
        print_Human(&member[i]);

    return 0;
}

C标准流

fopen解析:

fclose:

C
/*
    打开与关闭文件
*/

#include <stdio.h>

int main(void) {
    FILE *fp;

    fp = fopen("abc", "r"); /* 打开文件 */

    if (fp == NULL)
        printf("\a无法打开文件\"abc\"\n");
    else {
        printf("\a成功打开了文件\"abc\"\n");
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

也可以判断文件是否存在。

从文件读取数据:

C
/*
    读入身高和体重,计算并显示它们的平均值
*/

#include <stdio.h>

int main(void) {
    FILE *fp;
    int ninzu = 0;         /* 人数 */
    char name[100];        /* 姓名 */
    double height, weight; /* 身高,体重 */
    double hsum = 0.0;     /* 身高合计 */
    double wsum = 0.0;     /* 体重合计 */

    if ((fp = fopen("hw.dat", "r")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        while (fscanf(fp, "%s%lf%lf", name, &height, &weight) == 3) {
            printf("%-10s %5.1f %5.1f\n", name, height, weight);
            ninzu++;
            hsum += height;
            wsum += weight;
        }
        printf("----------------------\n");
        printf("平均       %5.1f %5.1f\n", hsum / ninzu, wsum / ninzu);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

文件写入 fprintf

C
/*
    向文件写出程序运行时的日期和时间
*/

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

int main(void) {
    FILE *fp;
    time_t current = time(NULL);            /* 当前日历时间 */
    struct tm *timer = localtime(&current); /* 分解时间(当地时间)*/

    if ((fp = fopen("dt_dat", "w")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        printf("写出当前日期和时间。\n");
        fprintf(fp, "%d %d %d %d %d %d\n", timer->tm_year + 1900, timer->tm_mon + 1, timer->tm_mday,
                timer->tm_hour, timer->tm_min, timer->tm_sec);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

scanf fscanf printf fprintf

文件流数量

最多能有多少数据流?

取决于操作系统,一个进程一般最多有256条数据流,数量有限,用完应用关闭它们。

当前日期与时间

C
/*
    向文件写出程序运行时的日期和时间
*/

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

int main(void) {
    FILE *fp;
    time_t current = time(NULL);            /* 当前日历时间 */
    struct tm *timer = localtime(&current); /* 分解时间(当地时间)*/

    if ((fp = fopen("dt_dat", "w")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        printf("写出当前日期和时间。\n");
        fprintf(fp, "%d %d %d %d %d %d\n", timer->tm_year + 1900, timer->tm_mon + 1, timer->tm_mday,
                timer->tm_hour, timer->tm_min, timer->tm_sec);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

显示程序上一次运行时的日期和时间:

C
/*
    显示程序上一次运行时的日期和时间
*/

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

char data_file[] = "datetime.dat"; /* 文件名 */

/*--- 取得并显示上一次运行时的日期和时间 ---*/
void get_data(void) {
    FILE *fp;

    if ((fp = fopen(data_file, "r")) == NULL) /* 打开文件 */
        printf("本程序第一次运行。\n");
    else {
        int year, month, day, h, m, s;

        fscanf(fp, "%d%d%d%d%d%d", &year, &month, &day, &h, &m, &s);
        printf("上一次运行是在%d%d%d%d%d%d秒。\n", year, month, day, h, m, s);
        fclose(fp); /* 关闭文件 */
    }
}

/*--- 写入本次运行时的日期和时间 ---*/
void put_data(void) {
    FILE *fp;
    time_t current = time(NULL);            /* 当前日历时间 */
    struct tm *timer = localtime(&current); /* 分解时间*/

    if ((fp = fopen(data_file, "w")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        fprintf(fp, "%d %d %d %d %d %d\n", timer->tm_year + 1900, timer->tm_mon + 1, timer->tm_mday,
                timer->tm_hour, timer->tm_min, timer->tm_sec);
        fclose(fp); /* 关闭文件 */
    }
}

int main(void) {
    get_data(); /* 取得并显示上一次运行时的日期和时间 */

    put_data(); /* 写入本次运行时的日期和时间 */

    return 0;
}

查看文件内容:

C
/*
    显示文件内容
*/

#include <stdio.h>

int main(void) {
    int ch;
    FILE *fp;
    char fname[FILENAME_MAX]; /* 文件名 */

    printf("文件名:");
    scanf("%s", fname);

    if ((fp = fopen(fname, "r")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        while ((ch = fgetc(fp)) != EOF)
            putchar(ch);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

putchar是显示到标准输出,fputc是写入到文件:

C
/*
    复制文件
*/

#include <stdio.h>

int main(void) {
    int ch;
    FILE *sfp;                /* 原文件 */
    FILE *dfp;                /* 目标文件 */
    char sname[FILENAME_MAX]; /* 原文件名 */
    char dname[FILENAME_MAX]; /* 目标文件名 */

    printf("打开原文件:");
    scanf("%s", sname);
    printf("打开目标文件:");
    scanf("%s", dname);

    if ((sfp = fopen(sname, "r")) == NULL) /* 打开原文件 */
        printf("\a原文件打开失败。\n");
    else {
        if ((dfp = fopen(dname, "w")) == NULL) /* 打开目标文件 */
            printf("\a目标文件打开失败。\n");
        else {
            while ((ch = fgetc(sfp)) != EOF)
                fputc(ch, dfp);
            fclose(dfp); /* 关闭目标文件 */
        }
        fclose(sfp); /* 关闭原文件 */
    }

    return 0;
}

在文本文件中保存实数

C
文件流问文件文件文件/*
    将圆周率的值写入目标文件,再进行读取
*/

#include <stdio.h>

int main(void) {
    FILE *fp;
    double pi = 3.14159265358979323846;

    printf("从变量pi得到的圆周率为%23.21f\n", pi);

    /* 写入操作 */
    if ((fp = fopen("PI.txt", "w")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        fprintf(fp, "%f", pi); /* 从pi写入 */
        fclose(fp);            /* 关闭文件 */
    }

    /* 读取操作 */
    if ((fp = fopen("PI.txt", "r")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        fscanf(fp, "%lf", &pi); /* 读取至pi */
        printf("从文件读取的圆周率为%23.21f\n", pi);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

浮点数的精度是有限的,因此变量pi的值并非初始值;再从文件中读取出来,精度还会更低。

文本文件与二进制文件

在二进制文件中保存实数

C
/*
    将圆周率的值写入二进制文件再进行读取
*/

#include <stdio.h>

int main(void) {
    FILE *fp;
    double pi = 3.14159265358979323846;

    printf("从变量pi得到的圆周率为%23.21f\n", pi);

    /* 写入操作 */
    if ((fp = fopen("PI.bin", "wb")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        fwrite(&pi, sizeof(double), 1, fp); /* 从pi写入 */
        fclose(fp);
    } /* 关闭文件 */

    /* 读取操作 */
    if ((fp = fopen("PI.bin", "rb")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        fread(&pi, sizeof(double), 1, fp); /* 读取至pi */
        printf("从文件读取的圆周率为%23.21f\n", pi);
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

显示文件自身:

C
/*
    用字符和字符编码显示文件内容
*/

#include <ctype.h>
#include <stdio.h>

int main(void) {
    int n;
    unsigned long count = 0;
    unsigned char buf[16];
    FILE *fp;
    char fname[FILENAME_MAX]; /* 文件名 */

    printf("文件名:");
    scanf("%s", fname);

    if ((fp = fopen(fname, "rb")) == NULL) /* 打开文件 */
        printf("\a文件打开失败。\n");
    else {
        while ((n = fread(buf, 1, 16, fp)) > 0) {
            int i;

            printf("%08lX ", count); /* 地址 */

            for (i = 0; i < n; i++) /* 十六进制数 */
                printf("%02X ", (unsigned)buf[i]);

            if (n < 16)
                for (i = n; i < 16; i++)
                    printf("   ");

            for (i = 0; i < n; i++) /* 字符 */
                putchar(isprint(buf[i]) ? buf[i] : '.');

            putchar('\n');

            count += 16;
        }
        fclose(fp); /* 关闭文件 */
    }

    return 0;
}

链表malloc free strdup示例

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

typedef struct island {
    char *name;
    char *opens;
    char *closes;
    struct island *next;
} island;

island *create(char *name) {
    island *i = malloc(sizeof(island));
    i->name = name;
    i->opens = "09:00";
    i->closes = "17:00";
    i->next = NULL;
    return i;
}

/*
create函数存在的问题:
name存储的是字符指针,实参是main函数栈里保存的字符数组的地址。
编译以后只保存地址,不管使用create多少次,都是共享栈里同一个char数组内容。
如果创建一次,不会出问题,创建多次,name会被覆盖,覆盖以后再去取值,则每次取到的是最新的值。

改进:不共享栈里面同一个值。可以复制栈值到堆里面。
string.h里面strdup()函数,专门复制值到堆里面。
char *name = "abc";
char *copy = strdup(name);
strdup()使用malloc,用完也要free()

改良成create2
*/

island *create2(char *name) {
    island *i = malloc(sizeof(island));
    i->name = strdup(name);
    i->opens = "09:00";
    i->closes = "17:00";
    i->next = NULL;
    return i;
}

void display(island *start) {
    island *i = start;
    for (; i != NULL; i = i->next)
        printf("Name: %s open: %s-%s\n", i->name, i->opens, i->closes);
}

void release(island *start) {
    island *i = start;
    island *tmp = NULL;
    while (i != NULL) {
        tmp = i->next; // 保存i->next到中间变量,不然free之后就可能有问题
        free(i->name);
        free(i);
        i = tmp;
    }
}

int main() {

    char name[80];

    printf("请输入第1个岛的名字:");
    fgets(name, 80, stdin);
    island *p_island0 = create2(name);
    // display(p_island0);
    printf("请输入第2个岛的名字:");
    fgets(name, 80, stdin);
    island *p_island1 = create2(name);
    p_island0->next = p_island1;
    printf("请输入第3个岛的名字:");
    fgets(name, 80, stdin);
    island *p_island2 = create2(name);
    p_island1->next = p_island2;

    display(p_island0);
    release(p_island0);

    return 0;
}