C语言笔记
大杂烩,一些学习笔记,看书摘抄、总结等,觉得有用、有复习温习价值的就记录下来。
C和C++标准库
标准库相关文档资料:
比较有权威参考性的网址:
- https://pubs.opengroup.org/onlinepubs/9699919799/
- https://www.cprogramming.com/
- https://en.cppreference.com/w/
- http://www.crasseux.com/books/ctutorial/
标准库的历史:
随着语言的发展新的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:
使用文档:
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/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风格。
其他参考:
- https://www.cnblogs.com/findumars/p/4145407.html
- https://www.cnblogs.com/yangwindsor/articles/3336255.html
clang-format与C代码风格
clang-format:
官网文档:
- https://clang.llvm.org/docs/ClangFormat.html
- https://clang.llvm.org/docs/ClangFormatStyleOptions.html
各个配置中文解释:
- https://blog.csdn.net/u013576331/article/details/135011351
- https://blog.csdn.net/softimite_zifeng/article/details/78357898
- https://blog.csdn.net/lishi_1991/article/details/135565891
在线预览配置的网站:https://clang-format-configurator.site
使用clang-format生产配置文件:
bash
clang-format -style=llvm -dump-config > .clang-format
主流C代码风格:
Linux 内核代码风格,可以多读几遍,重点参考:
google的风格:
- https://zh-google-styleguide.readthedocs.io/en/latest/
- https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/formatting.html
还有其他的代码风格,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(¤t); /* 分解时间(当地时间)*/
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(¤t); /* 分解时间(当地时间)*/
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(¤t); /* 分解时间*/
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;
}