《嗨翻C语言》(上)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《嗨翻C语言》(上)相关的知识,希望对你有一定的参考价值。
说明
一、课外
三种C标准
windows下使用gcc
其他
二、基础知识
表达式
存储器
指针
字符串
数组
结构(结构体)
联合
枚举
位字段
递归
三、分而治之
小工具
输入输出
创建自己的数据流
命令行参数
使用多个源文件
四、动态存储
说明
一、课外
三种C标准
windows下使用gcc
其他
二、基础知识
表达式
存储器
指针
字符串
数组
结构(结构体)
联合
枚举
位字段
递归
三、分而治之
小工具
输入输出
创建自己的数据流
命令行参数
使用多个源文件
四、动态存储
2016/2/23 14:41:06
《嗨翻C语言》,本书分为三个部分。
本书分为三个部分:第1章到第4章是基础知识,包括基本语法、指针、字符串、小工具和源文件;第5章到第8章为进阶内容,有结构、联合、数据结构、堆、函数指针、动/静态链接;最后四章是高级主题,内容涵盖了系统调用、进程间通信、网络编程和多线程。(加粗部分的笔记在下篇)
以下笔记没有严格按照章节进行整理,因为书中在多个章节可能提到同一类知识点,比如(略)
说明
在此笔记中,不记录不同C标准的差异,具体细节查看原书;不记录不同OS下编写C时的差异,具体细节查看原书或者google。笔记中只对不同标准、不同OS下共性的语言原理、语法细节做出整理。
此上篇整理了1-190项标注和笔记。
一、课外
三种C标准
ANSI C 始于20世纪80年代后期,适用于最古老的代码;1999年开始的C99标准有了很大的改进;在2011年发布的最新标准C11中……想知道编译器支持哪种标准,可以查看编译器的文档。
“指定初始化器”按名设置结构和联合字段,它属于C99标准;
C99标准支持“指定初始化器”,C++不支持。
在代码中间的位置声明新变量,只有C99和C11标准才允许这样做,在ANSI C(在此特指 C89)中,必须在函数的顶部声明局部变量;
扩展阅读:
windows下使用gcc
虽然在本书中作者提供了以下两种方法,但是在后续的介绍中也多有提及其不足之处。而且在接触之后,我还是放弃了 Cygwin,转而在虚拟机上安装了 debian。
如在Windows操作系统上使用gcc(GNU编译器套装),有两种选择:一种是 Cygwin,它可以完全模拟UNIX环境,自然也就包括了gcc;如果你只是想创建能够在Windows下运行的程序,MinGW 可能更符合你的需要。
其他
面向对象是一种对抗软件复杂性的技术。
二、基础知识
表达式
在C语言中,几乎每样东西都有返回值:表达式 x = 4
本身也有一个值,这个值是4,即赋给x的值。
Q1:这个赋值表达式能否进行再次赋值?例如 (x=3)=4
A1:在C中编译无法通过,类似3=4
;在C++中正常编译,运行,类似x=4
。
注意:以下这种再次赋值是成立的。因为无论 y=4
(C语言) 还是 y=x
(C++) 都是成立的。
int main()
{
int x = 9;
int y = 8;
// 方式一
y = x = 4;
// or 两者意义相同
// y = (x = 4);
printf("x is %d, y is %d\n", x, y);
return 0;
}
关键是理解 C/C++中左值 的概念,而且 C 和 C++ 中是不一样的。
- 使用switch语句的好处之一是,可以用下落逻辑在不同的分支之间复用代码。
存储器
在C语言中,几乎每样东西都有返回值:表达式 x = 4
本身也有一个值,这个值是4,即赋给x的值。
Q1:这个赋值表达式能否进行再次赋值?例如 (x=3)=4
A1:在C中编译无法通过,类似3=4
;在C++中正常编译,运行,类似x=4
。
注意:以下这种再次赋值是成立的。因为无论 y=4
(C语言) 还是 y=x
(C++) 都是成立的。
int main()
{
int x = 9;
int y = 8;
// 方式一
y = x = 4;
// or 两者意义相同
// y = (x = 4);
printf("x is %d, y is %d\n", x, y);
return 0;
}
关键是理解 C/C++中左值 的概念,而且 C 和 C++ 中是不一样的。
如果真的想玩转C语言,就需要理解C语言如何操纵存储器。掌握指针和存储器寻址对成为一名地道的C程序员很重要。
函数(例如 main() 函数)中声明变量,计算机会把它保存在一个叫栈(Stack)的存储器区段中;函数以外的地方声明变量,计算机则会把它保存在存储器的全局量段(Globals);堆用于动态存储。
存储器的分布图:参考位置#1022、位置#1322、位置#1448
- 存储器是进程的
sizeof
是运算符,好比+
、&
,它不是库函数。程序是在编译期间计算sizeof
的。sizeof
运算告知某样东西在存储器中占多少字节,既可以对数据类型使用,也可以对某条数据使用。
指针
- 指针就是存储器中某条数据的地址
- 指针做了两件事:避免副本和共享数据。
- 指针是一种间接形式的地址(怎么理解?)
*
运算符可以读取存储器地址中的内容。*
运算符还可以设置存储器地址中的内容。
字符串
指向字符串字面值(string literal)的指针变量不能用来修改字符串的内容
- 在存储器的非只读区域创建了字符串的副本,就可以修改它的字母
如果你想把指针设成字符串字面值,必须确保使用了
const
关键字:// bus error 运行时崩溃
char *card = "JQK";
card[1] = ‘A‘;
编译错误,比在运行时崩溃好太多了!
// 编译不通过
const char *s = "some string";
s[0] = ‘S‘;
加不加
const
,字符串字面值都是只读的,const
修饰符表示,一旦你试图用const
修饰过的变量去修改数组,编译器就会报错// 通过jimmy 修改内容就会报编译错误,
// 但是通过masked_raider 修改就可以成功。
char masked_raider[] = "Alive";
const char *jimmy = masked_raider;
数组
- 数组的索引值是一个偏移量
数组变量好比指针……
char quote[] = "Cookies make you fat";
计算机为字符串的每一个字符以及结束字符
\0
在栈上分配空间,并把首字符的地址和 quote 变量关联起来。函数传参时传给函数的是指针。数组变量与指针又不完全相同(区别:重点理解2、3点)
sizeof(数组)
是……数组的大小;sizeof(指针)
返回4或8。数组的地址……是数组的地址;指针的地址是另一个地址。
&s == s; // 数组变量
&t != t; // 指针
数组变量不能指向其他地方。
计算机会为数组分配存储空间,但不会为数组变量分配任何空间。
指针退化
把数组赋给指针变量,指针变量只会包含数组的地址信息,而对数组的长度一无所知,相当于指针丢失了一些信息。我们把这种信息的丢失称为退化。
结构(结构体)
结构可以像数组那样在结构中保存字段,但读取时只能按名访问。
Q1:数组变量就是一个指向数组的指针,那么结构变量是一个指向结构的指针吗?
A1:不是,结构变量是结构本身的名字。 数组变量的地址是数组变量自身;结构变量的地址是指针,不是自身。
为结构变量赋值相当于叫计算机复制数据。
重点在函数传参时很可能浪费较多存储资源;而数组作为函数参数传的是指针。
用
typedef
为结构创建别名。用typedef
定义结构时可以省略结构名,只写类型名。// 定义结构体
struct call_phone {
int cell_no;
const char *wallpaper;
float minutes_of_charge;
};
// 创建别名
typedef struct call_phone {
int cell_no;
const char *wallpaper;
float minutes_of_charge;
} phone;
// 省略结构名
typedef struct {
int cell_no;
const char *wallpaper;
float minutes_of_charge;
} phone;
关于“对齐”,可以查看原文或者进一步从网上学习
在C语言中,参数按值传递给函数。
作者要表达的就是字面意思[email protected]但这句话在国内出版物中经常作为前半句出现—“值传参和“指针传参”,以至于之前理解都有偏差。其实“指针传参”还是按值传递给函数的。
联合
- 定义一种叫“量”的数据类型,然后根据特定的数据决定要保存个数、重量还是容积。
每次创建结构实例,计算机都会在存储器中相继摆放字段,联合则不同。当定义联合时,计算机以其中最大的字段分配空间,然后由你决定里面保存什么值。
typedef union {
short count;
float weight;
float volume;
} quantity;
指定初始化器(designated initializer)按名设置结构和联合字段,它属于C99标准
quantity q = {.weight=1.5};
phone p= {.cell_no=15210, .minutes_of_charge=1.2};
联合提供了一种让你创建支持不同数据类型的变量的方法。
联合经常和结构一起用。创建联合相当于创建新的数据类型。
编译器不会记录你在联合中设置或读取过哪些字段。
Q1:可以在
union
中保存任何字段(count
、weight
或者volume
)的值,这些不同类型的值保存在存储器中相同的位置……既然如此,你怎么知道我保存的是float
还是short
?要是我保存了float
字段,却读取了short
字段呢?A1:解决方法:只要用枚举或其他东西记录一下就行了。
枚举
(略)
位字段
- 位字段(bitfield)的初衷是节省存储器空间:真/假的值只需要一位就能表示;月份等小范围的数字……
只有当多个位字段出现在同一个结构中,才能节省空间。
如果编译器发现结构中只有一个位字段,还是会把它填充成一个字,这就是为什么位字段总是组合在一起。
可以用位字段指定一个字段有多少位。位字段应当声明为
unsigned int
typedef struct {
// 是否第一次参观?
unsigned int first_visit:1;
// 还会再来吗?
unsigned int come_agin:1;
// 被咬掉了几根手指?
unsigned int fingers_lost:4;
// 被鲨鱼袭击过吗?
unsigned int shark_attack:1;
// 一周来几天?
unsigned int days_a_week:3;
} survey;
Q1:为什么C语言不支持二进制字面值?
A1:因为二进制字面值占了很大空间(质疑?),而且十六进制通常写起来更快。
递归
为了保存可变数量的数据,需要一样比数组更灵活的东西,即链表。
链表是一种抽象数据结构。链表是通用的,可以用来保存很多不同类型的数据,所以被称之为抽象数据结构。
与数组相比,链表还有一个优点:插入数据非常快。
Q1:如何在C语言中创建链表?
A1:通过创建递归结构实现。
如果一个结构包含一个链向同种结构的链接,那么这个结构就被称为递归结构。
在递归结构中,需要包含一个相同类型的指针,C语言的语法不允许用typedef别名来声明它。个人猜测,应该和声明顺序有关。毕竟如果
typedef 新类型 newtype
,然后newtype * pointer
创建指针,是可行的。// 在递归结构中,必须为结构体命名。别名可选
typedef struct island {
char *name;
char *opens;
char *closes;
struct island *next; // 此处不允许使用别名来声明
} island;
>
C语言需要知道结构在存储器中占的具体大小,如果在结构中递归地复制它自己,那么两条数据就会不一样大。指针的大小是确定的。
- 在C语言中,
NULL
的值实际上为0
三、分而治之
小工具
问:什么是小工具?——其实就是其字面意思,重点是要有这个概念。
操作系统都有小工具。类Unix的操作系统为完成工作会大量使用工具,Windows用的少一些。
C语言小工具执行特定的小任务,例如读写文件、过滤数据。如果要完成更复杂的任务,可以把多个工具链接在一起。
小工具是一个C程序,它做一件事情并把它做好。
当你想解决一个大问题时,可以把它分解成一连串的小问题,然后针对每个小问题写一个小工具。
问:为什么小工具要使用标准输入和标准输出?
答:有了它们,就可以轻易用管道将小工具们串连起来。
问:如果两个程序用管道相连,第二个程序要不要等第一个程序执行完后才能开始运行?
答:不需要,两个程序可以同时运行,第一个程序一发出数据,第二个程序马上就可以处理。
输入输出
使用
scanf()
时要小心:限制scanf()
能读入的字符数// 注意字符数组的长度,和scanf 读取的限制
int main()
{
char card_name[3];
puts("输入牌名:");
scanf("%2s", card_name);
// other code
return 0;
}
如果忘了限制
scanf()
读取字符串的长度,用户就可以输入远远超出程序空间的数据,多余的数据会写到计算机还没有分配好的存储器中如果你要向
fgets()
函数传递数组变量,就用sizeof
;如果只是传指针,就应该输入你想要的长度。- 操作系统控制数据如何进出标准输入、标准输出。
scanf()
和printf()
函数,只管从标准输入读数据,向标准输出写数据- 进程有一只耳朵(标准输入)和两张嘴(标准输出和标准错误)
printf()
其实只是fprintf()
函数的特例,- 可以用
>
重定向标准输出,2>
重定向标准错误。 - 用管道连接输入与输出
创建自己的数据流
程序运行时,操作系统会为它创建三条数据流:标准输入、标准输出和标准错误。但有时你需要创建自己的数据流。
每条数据流用一个指向文件的指针来表示,可以用
fopen()
函数创建新数据流。创建数据流后,可以用
fprintf()
函数往数据流中打印数据;可以用fscanf()
函数从数据流中读取数据。当用完数据流,别忘了使用
fclose()
函数关闭它。虽然所有的数据流在程序结束后都会自动关闭,但你仍应该自己关闭它们:
命令行参数
十个程序有九个需要选项。聊天程序有“系统设置”,游戏有调整难度的选项,而命令行工具需要有命令行选项。
很多程序都会用到命令行选项,因此有一个专门的库函数,可以使用它来简化处理流程。这个库函数叫做
getopt()
,每一次调用都会返回命令行中下一个参数。有关函数的具体使用方法,翻阅原书或者 google 都行。
unistd.h头文件不属于C标准库,而是POSIX库中的一员。POSIX的目标是创建一套能够在所有主流操作系统上使用的函数。
使用多个源文件
- 当编译器看到尖括号,就会到标准库代码所在目录查找头文件;用引号把文件名括起来,编译器就会在本地查找文件。
C语言是一种很小的语言,共有32+n个保留字。以下列出部分:
auto register static extern typedef union volatile
使用头文件
(函数)声明与定义分离
将声明和定义分离之后也不用再严格调整函数定义之间的顺序。
把声明放到一个独立的头文件中有两大优点,第一是主代码变短了,第二可以共享代码(被两个以上源文件 include 时)。
如此,就可以在不同的文件之间共享函数了,但如果你想共享变量呢?
为了防止两个源文件中的同名变量相互干扰,变量的作用域仅限于某个文件内。如果你想共享变量,就应该在头文件中声明,并在变量名前加上
extern
关键字(需要进一步的学习):ps:原文表述不太恰当。改为:
- 为了防止两个源文件中的同名变量相互干扰,变量的作用域仅限于某个文件内——(这句话存在异议,使用 gcc 实际编译时不同文件使用同名变量会报错“重复定义”)。
- 如果你想共享变量,就应该在头文件中声明。——声明变量,即在变量名前使用
extern
关键字,不应该用“并”。 - 关于
extern
,见《进一步学习extern》笔记。
四、动态存储
- ——另起篇幅
以上是关于《嗨翻C语言》(上)的主要内容,如果未能解决你的问题,请参考以下文章
我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段
我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段