本文是阅读《C Primier Plus(第6版)》时的一些笔记,仅作记录
PDF文件网盘链接:https://pan.baidu.com/s/1gyR0xmNStsE8MaWI2mi4Hw?pwd=6999
1 C概述
C语言在B语言的基础上进行设计而来,是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发。
-
C标准
美国国家标准协会(ANSI) 1989/1990——C89/C90; 1994——C99; 2007——C11 -
编程的7个步骤
- 定义程序的目标
- 设计程序
- 编写代码
- 编译程序
- 运行程序
- 测试和调试程序
- 维护和修改
-
C程序源文件:一个项目中包含
main()
函数的.c
文件,是整个编译过程的开始 -
编译过程:
- 预处理:
.c/.cpp→.i
- 编译:
.i
文件–>.s
文件 - 汇编:
.s
文件–>.o
文件 - 链接:
.o
文件–>bin文件
- 预处理:
其他关于GNU的gcc、g++编译器、Linux OS编译命令见:https://blog.csdn.net/Morejay/article/details/128515728
#include "stdio.h" // C程序的 标准输入输出 头文件
// C常用IO函数
int getchar()//从标准输入设备写入一个字符
int putchar()//向标准输出设备读出一个字符
int scanf(char* format[, argument…])//从标准输入设备读入格式化后的数据
int printf(char* format[, argument…])//向标准输出设备输出格式化字符串
char* gets(char* string)//从标准输入设备读入一个字符串
int puts(char* string)//向标准输出设备输出一个字符串
int sprintf(char* string, char* format[, …])//把格式化的数据写入某个字符串缓冲区
引入头文件使用""
和<>
的区别:
- 引用的头文件不同
#include <>
引用的是编译器的类库路径里面的头文件。
#include ""
引用的是用户当前程序目录的相对路径中的头文件。 - 用法不同
#include <>
用来包含标准头文件(例如stdio.h
或stdlib.h
).
#include ""
用来包含非标准头文件。 - 调用文件的顺序不同
#include <>
编译程序会先到标准函数库中调用文件。
#include ""
编译程序会先从当前目录中调用文件。 - 预处理程序的指示不同
#include <>
指示预处理程序到预定义的缺省路径下寻找文件。
#include ""
指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。
简单C程序示例:
#include <iostream> // iostream是C++输入输出流类,兼容C和C++,C程序适用stdio.h头文件即可
using namespace std; // CPP程序的标准输入输出 命名空间,可以不使用,否则下面代码要修改如下:
// std::cout << "C++ ..." << std::endl;
int avoid_conflict_modify_main() {
printf("c ...\n");
cout << "C++ ..." << endl;
int num; // 变量声明
num = 6; // 变量赋值
printf("my favorite number is %d .", num);
getchar(); // 该行读取一次键的按下,以保障程序暂停,不会出现程序窗口一闪而过的情况
return 0;
}
2 数据和C
- 关键字:int、short、long、unsigned、char、float、double、【C99】_Bool(0 or 1 只占一位存储空间)、_Complex(复数)、_Imaginary(虚数)
- 运算符:
sizeof()
- 函数:
scanf()
- 整数类型和浮点数类型的区别
- 如何书写整型和浮点数类型,如何声明这些类型的变量
- 如何使用printf()和scanf()函数读写不同类型的值
通过C程序了解上面的概念:
注意下面的代码中在
strcpy()
及scanf()
函数名加入_s
在适用Visual Studio编译C程序时,strcpy() scanf()会报错?????
- 解决方案:
- 使用在方法后加
_s
—— VS编译器提供的函数,非C语言库提供的标准函数 (不建议,只适用VS编译器)- 在代码的最顶端输入
#define _CRT_SECURE_NO_WARNINGS
- 在VS的界面中,最顶端找到项目,点击属性,找到C/C++这一栏,选择预处理器,打开预处理器定义这一栏最右边的小三角,选择编辑。在最上方的白框中输入_CRT_SECURE_NO_WARNINGS,点击确定,然后点击应用
#include <stdio.h>
void differentDataTypeOutput(); // 函数定义
int avoid_conflict_2_main() {
float weight; // float精度高于int 高精度向低精度转换可能会丢失数据(double -> int)
float value;
// 获取用户的输入
scanf_s("%f", &weight); // &weight把输入的值赋给名为weight的变量 &用于找到变量的地点
value = 1700.0 * weight * 14.5833;
printf("weight is worth $ %.2f.\n", value); // .2f用于精确控制输出,指定输出浮点数只显示小数点后两位
// %d--显示整型;%o--八进制(0oo,o:0~7);%x--十六进制(0xhh、0Xhh,h:0~f)
differentDataTypeOutput();
return 0;
}
void differentDataTypeOutput() {
// 个人计算机常见:long long占64位;long占32位;short占16位;int占16或者32位
unsigned int un = 3000000000; /* int为32位和short位16位的系统*/
short end = 200;
long big = 65537;
long long verybig = 12345678908642;
printf("un = %u and not %d\n", un, un);
printf("end = %hd and not %d\n", end, end); // 给函数传参时,编译器把short的值自动转换成int
printf("big = %ld and not %hd\n", big, big);
printf("verybig = %lld and not %ld\n", verybig, verybig);
}
C99新增两个头文件stdint.int
和inttypes.h
,以确保C语言的类型在各系统中的功能相同
如
int32_t
表示32位的有符号整数类型,使用32位int的系统中,头文件会把int32_t
作为int
的别名
float some;
some = 4.0 * 2.0;
// 编译器默认假定浮点型常量为(64位)double类型,使用双精度进行乘法运算,然后将乘积截断成float
// 这样做的计算精度更高,但是会减慢程序的运行速度
在浮点型常量后加f
orF
可以覆盖默认设置。编译器会将其看作float
类型
4.0f // float类型
4.0L or 4.0l // long double
printf()
函数打印浮点值——%e
打印指数计数法的浮点数
C99:系统支持十六进制格式的浮点数,可以用a
和A
分别代替e
和E
浮点值的上溢和下溢:
- 计算导致数值过大,超过当前类型所能表达的范围时,发生上溢:赋无穷大
printf()
显示 inf 或 infinity - 一个很小的数,假设指数已经是最小值了,如:0.1234E-10再除以10,得到结果0.0123E-10,虽然得到了结果但是末尾损失了有效数字——下溢
- 特殊浮点值:NaN(Not a Number)
C语言的3种复数类型:float_Complex
、double_Complex
、long double_Complex
C语言的3种虚数类型:float_Imaginary
、double_Imaginary
、long double_Imaginary
需要包含
complex.h
头文件,便可以用complex
代替_Complex
,用Imaginary代替_Imaginary
,用I
代替-1
的平方根
字符串类型string
#include <stdio.h>
#include <string.h> // 字符串库函数,提供strlen()函数的原型
#define DENSITY 62.4 // 预处理指令,末尾无';' 定义人体密度(磅/立方英尺),编译时会将 DENSITY 替换成 62.4
/* 也可以是适用 float density = 62.4; 但是这样做程序可能会在无意间改变它的值*/
int avoid_3_main() {
float weight, volume;
int size, letters;
char name[40]; // 可容纳40个字符的数组,C语言没有专门用于存储字符串的变量类型,C语言字符串以'\0'结尾
printf("what's your name?\n");
//strcpy(name, "moonjay");
scanf("%s", name);
printf("%s, what's your weight in pounds?\n", name);
scanf("%f", &weight);
size = sizeof name;
letters = strlen(name);
volume = weight / DENSITY;
printf("well, %s, your volume is %2.2f ciubic feet.\n", name, volume);
printf("your name has %d letters, and we have %d bytes to store it.\n", letters, size);
return 0;
}
void explain_const() {
// C90
//
}
C90标准新增了const关键字,用于限定一个“变量”为只读
上面代码中预定义的 DENSITY
属于常量,即符号常量
const int MONTHS = 12; // MONTHS在程序中不可更改
/* C头文件 limits.h和float.h分别提供了与整数类型和浮点数类型大小限制相关的详细信息
在 limits.h 中:
#define INT_MAX +32767
#define INT_MIN -32768
*/
数组是按顺序存储的一系列类型相同的值
字符数组【不以
'\0'
结尾】与字符串【以'\0'
结尾,也是字符数组】有些微区别
char char_array[20]; // 数组声明
3 符号和语句
- 关键字:while、typedef
- 运算符:=、-、*、/、%、++、-- 、sizeof
- 复合语句、自动类型转换和强制类型转换
#define SCALE 2.7;
// 表达式的计算顺序【() 的优先级最高,先计算 () 的部分】
float butter, n = 6.0;
butter = 25.0 + 60.0 * n / SCALE; // 计算遵循从左到右的顺序
// 先 60.0 * n 得temp1;再 temp1 / SCALE 得temp2;最后 25.0 + temp2 结果赋值给 butter
int a = 1, b = 1, a_post, b_pre;
a_post = a++; // 后缀:使用a的值之后,递增a
b_pre = ++b; // 前缀:使用b的值之前,递增b
/* ++ -- 优先级仅次于 () ; ,运算符从右向左执行*/
size_t
类型
size_t intsize; // sizeof返回size_t类型的值【无符号整数类型】,它不是新类型 是(typedef)定义的标准类型
intsize = sizeof(int); // 可以使用 %zd 显示 size_t类型的值 【也可用 %u、%lu】
/*上面一行代码等价于下面两行*/
int n;
sizeof n;
typedef
允许为现有类型创建别名
typedef double real;
real num; // num为double类型
类型的转换【自动转换和强制转换】
/*类型转换的 升级(低精度向高精度) 通常不会有什么问题; 降级会导致麻烦*/
float mice = 1.6 + 1.7;
float mice_2 = (int)1.6 + (int)1.7; // 强制类型转换
4 循环、条件判断与运算符
- 循环关键字:
for
、while
、do......while
do
statement
while( expression )
// 至少会执行一次循环
for(初始化;判断;更新){ 循环体 }
while(判断){ 循环体 }
- 条件判断关键字:
if、else、switch、continue、break、case、default、goto
- 关系运算符与算术运算符(前者优先级低于后者,但两者都高于
=
):
>、<、>=、<=、!=、==
&&、||、 ?:
+= -= *= /= %=
使用
iso646.h
头文件后,可以用and
代替&&
、or
代替||
、not
代替!
#include <stdio.h>
#include <ctype.h>
void func_char_1() {
char ch = getchar();
// 该函数不带任何参数,它从输入队列中返回下一个字符 等价于 scanf("%c", &ch);
// putchar和getchar**只处理字符**,它们比通用的scanf和printf更快
while (ch != '\n') { // 输入字符以 换行符 结束
if (ch == ' ') putchar(ch);
// putchar()函数打印它的参数 等价于 printf("%c", ch);
else putchar(ch + 1);
ch = getchar(); // 获取下一个字符
}
putchar(ch);
// 上面的程序 会将句子中间、结尾的标点也转换。解决此问题 需要使用ctype.h【包含isalpha()的函数原型】
}
void func_cahr_2() {
char ch; // 该函数不带任何参数,它从输入队列中返回下一个字符 等价于 scanf("%c", &ch);
while ((ch = getchar()) != '\n') {
if (isalpha(ch)) putchar(ch+1); // 如果是字符 显示它的下一个字符
else putchar(ch); // 不是则原样显示
}
putchar(ch);
// 此外 tolower(ch)、toupper(ch)会将字符转换成小写、大写字符
// ctype.h还有很多【字符测试函数】
}
5 输入/输出
- 输入、输出以及缓冲输入和无缓冲输入的区别
缓冲输入和无缓冲输入
- 无缓冲输入:回显用户输入的字符后立即重复打印该字符(正在等待的程序可立即使用输入的字符)
- 缓冲输入:用户在按下
[Enter]
之前不会重复打印刚输入的字符,用户输入的字符被收集存储在缓冲区(buffer)
- 如何通过键盘模拟文件结尾条件
现代文本文件不一定有嵌入的
Ctrl+Z
,如果有一定会将其是为一个文件结尾标记
C语言在 getchar()和scanf()函数检测到文件结尾时都会返回EOF(End of file) ,定义在stdio.h
中——#define EOF (-1)
手动输入时,想要结束文件,不能通过输入EOF或者-1的方式,而是:Ctrl+D
【Unix】Ctrl+Z
【PC】
C语言处理文件——C程序处理的是流(stream)而不是直接处理文件
流是一个实际输入或输出映射的理想化数据流。打开文件的过程就是把流与文件相关联。
C把输入和输出设备视为存储设备上的普通文件
stdin流和stdout流分别表示键盘输入和屏幕输出
getchar()、putchar()、printf()、scanf()都是标准I/O包成员,处理这两个流
- 如何通过重定向把程序和文件相连接(假设prog是可执行程序名 file1、file2是文件名)
- 把输出重定向至文件:
>
prog >file1
- 把输入重定向至文件:
<
prog <file2
- 组合重定向:(把file2作为输入、file1作为输出)
prog <file2 >file1
prog >file1 <file1
- 把输出重定向至文件:
- 函数:
fopen()、getc()、putc、exit、fclose、
fprintf、fscanf、fgets、fputs、
rewind、fseek、ftell、fflush、
fgetpos、fsetpos、feof、ferror、
ungetc、setvbuf、fread、fwrite
- 文件模式和二进制模式【C的两者文件模式】、文本和二进制格式、缓冲和无缓冲IO
文件通常是在磁盘或固态硬盘上一段已命名的存储区,C把文件看作是一系列连续的字节,每个字节都能被单独读取
底层I/O【OS提供的基本I/O服务】,标准高级I/O【使用C库的标准包和stdio.h
头文件定义,比底层I/O有优势】
C程序会自动打开3个文件:标准输入(standard input)、标准输出(standard output)和标准错误输入(standard error output)
通过下面的C程序了解上述概念:
#include <stdio.h>
#include <stdlib.h> // 提供exit()函数的原型
void read_file(int argc, char * argv[]) {
int ch; // 读取文件是,存储每个字符
FILE* fp; // 文件指针
unsigned long count = 0;
if (argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE); // EXIT_FAILURE【失败结束程序】——宏定义在stdlib.h中
}
if ((fp = fopen(argv[1], "r")) == NULL) // fopen(file_name, 打开方式)
{
/* "r"读模式打开
"w"写模式打开,把现有文件的长度截为0,若文件不存在,则创建一个新文件
"a"写模式打开,在现有文件末尾添加内容,若文件不存在,则创建一个新文件
"r+"更新模式打开(读写)
"w+"更新模式打开,把现有文件的长度截为0,若文件不存在,则创建一个新文件
"a+"更新模式打开,在现有文件末尾添加内容,若文件不存在,则创建一个新文件
"rb"、"wb"、"ab"、"ab+"、"a+b"、"wb+"、"w+b" 二进制模式
C11新增带x的写模式 "wx"、"wbx"、"w+x"、"wb+x"
- 若以传统的一种写模式打开一个现有文件、fopen会把该文件的长度截为0【丢失了文件内容】
使用带x的写模式,既是fopen失败,原文件内容也不会被删除
- 若环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件
fopen成功打开文件后会返回 文件指针
*/
printf("Can not open: %s filename\n", argv[1]);
exit(EXIT_FAILURE);
}
while ((ch = getc(fp))!=EOF)
{
/* getc()和putc()函数与getchar()和putchar()函数类似,不同的是要告诉getc() putc()函数使用哪一个文件
*
* ch = getchar(); 表示从标准输入中获取一个字符
* ch = getc(fp); 表示从fp指定的文件中获取一个字符
*/
putc(ch, stdout); // stdin、stderr
count++;
}
fclose(fp); // 关闭fp指定的文件,必要时刷新缓冲区【成功关闭返回0、否则返回EOF】
printf("File %s has %lu charactrers\n", argv[1], count);
}
fprintf()
和fscanf()
工作方式与printf()
和scanf()
类似,需要用第一个参数指定待处理的文件
fprintf(stdout, "can't open ... file.\n");
while((fscanf(stdin, "%40s", words) == 1) && (words[0] != '#')) // 标准输入流键入文字,new line的'#'结束输入
fprintf(fp, "%s\n", words);
rewind(fp); // 返回文件开始处
fgets(buf, STLEN, fp)
和fputs(字符串地址buf, fp)
这里的buf是char类型数组的名称,fgets函数读取输入直到第一个换行符的后面,或读到文件结尾、或读取STLEN-1个字符
fputs在打印字符串是不会在其末尾添加换行符
fseek()
【返回int类型】和ftell()
【返回long类型:文件中当前位置】
fseek()把文件看作是数组,在fopen()打开的文件中直接移动到任意字节处
fseek(fp, 0L【偏移量】, SEEK_END【起始点】); //定位到文件末尾 SEEK_CUR当前位置、SEEK_SET文件开始处
long last = ftell(fp);
fseek(fp, -count, SEEK_END); // count为long类型,从文件末尾出回退
/*
int ungetc(int c, FILE * fp)函数把C指定的字符放回输入流中(这样下次调用标准输入函数时将读取该字符)
int fflush(File * fp)刷新缓冲区,输出缓冲区中所有未写入数据被发送到fp指定的输出文件。若fp为空指针,所有输出缓冲区都被刷新
int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size) 创建了一个供标准I/O函数替换使用的缓冲区
fp指向待处理的流,buf指向待使用的存储区【若buf值不为NULL,则必须创建一个缓冲区】
mode: _IOFBF完全缓冲(缓冲区满时刷新)
_IOLBF行缓冲(缓冲区满时或写入下一个换行符时)
_IONBF无缓冲(操作成功返回0.否则返回一个非0值)
*/
fread()
和fwrite()
之前介绍的标准I/O函数都是面向文本的(用于处理字符和字符串),要在文件中保存数值数据首先想到fprintf()函数
double num = 1. / 3.;
fprintf(fp, "%2.f", num); //将结果存储为4个字符"0.33"【转化为字符串存储】。改变转换说明将改变存储该值需要的空间,导致存储不同的值
// 为了保证数据在存储前后一致,如存储数据需要以二进制形式存储,这时需要fread()和fwrite()函数
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp)
size_t fread(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp) //ptr为待读取文件在内存中的地址
/* 标准输入函数返回EOF、读取错误的情况*/
int feof(FILE * fp)和int ferror(FILE * fp)
}
6 函数与指针
- 一元运算符:
*、&
- 函数定义、使用参数和返回值、指针变量作为函数参数 【组织程序的思想就是把函数用作构件】
- 递归
通过一个简单的C程序了解以上概念:
#include <stdio.h>
#define WIDTH 40
/*1.函数原型 没有使用starbar(void)形式指定该函数确实没有参数,##它将不会检查参数##*/
void starbar();
void func_pointer(int u, int v);
void func_pointer_2(int* u, int* v);
void up_and_down(int n) { // 既是函数原型也是函数定义
printf("Level %d: n location %x\n", n, &n); // %x(不推荐)
// %p:000000CF06EFFB00(win64)、001DFDF8(win32) %x:85affab0(win64)、12ff79c(win32)
// %p来输出指针(无符号整数)的值、输出地址符 使用64位编译器会打印8个字节(64位),左边空缺会补零
// 如果编译器不识别%p 用%u或者%lu代替
if (n < 4) // 递归函数一定要定义出口条件,否则会造成系统栈溢出
{
up_and_down(n + 1); // 递归调用
}
}
int avoid_main_8(){
int x = 5, y = 8;
printf("below is starbar.\n");
starbar(); /*3.函数调用*/
//int imax(int, int); /* 还可以在主调函数中声明函数,该函数接受两个int型的参数*/
up_and_down(1);
printf("in main:originally x = %d, y = %d\n", x, y);
func_pointer(x, y);
printf("in main:after swap x = %d, y = %d\n", x, y);
printf("use pointer in main:originally x = %d, y = %d\n", x, y);
func_pointer_2(&x, &y); // 注意:这里传的参数是地址
printf("use pointer in main:after swap x = %d, y = %d\n", x, y);
return 0;
}
void starbar(){ /*2.函数定义*/
int count;
for (count = 0; count < WIDTH; count++) {
putchar('*');
}
putchar('\n');
}
指针初识(交换两数的位置):
指针是C语言中最重要(复杂)的概念之一,用于储存变量的地址。
一元运算符&
给出变量储存的地址
void func_pointer(int u, int v) {
int temp;
printf("in func:originally u = %d, v = %d\n", u, v);
temp = u;
u = v;
v = temp;
printf("in func:after swap u = %d, v = %d\n", u, v);
// 这里最后显示的结果是:在方法内两数成功交换,但是在main方法调用后,两数却没有交换
// 问题出在把结果传回mian()时,函数func_pointer使用的并不是main中的变量(x,y),因此交换u,v对x,y的值没有影响
}
void func_pointer_2(int *u, int *v) {
/* 对上述交换算法的指针形式改进,利用指针在函数间通信
使用间接运算符'*'
int *pi; // pi是指向int类型变量的指针
假设pi指向int型变量val,则 pi == &val;
int temp = *pi; // 找出pi指向的值赋给temp
*/
int temp;
temp = *u;
*u = *v; // 区别在于,这里将指定地址的值进行交换,不会出现上一个函数的交换不同变量的问题
*v = temp;
}
二维数组(探讨此时指针与数组的关系)
假设arr是二维数组,
arr[0]
是该数组首元素arr[0][0]
的地址,所以*(arr[0]) == arr[0][0]
类似地:*arr
代表一维数组首元素arr[0]
,代表二维数组首元素的地址&arr[0][0]
static int arr[6] = {[5] = 212 }; // 静态变量。C99 设置 arr[5]
**arr == *&arr[0][0]
指针数组与数组指针
int (* pi)[2]; // pi指向一个内含两个int类型值的数组,pi为指向一个数组的指针 【数组指针】
// pi[i]代表一个由2个整数构成的元素,pi[i][j]代表一个整数
int * pz[2]; // pz是一个内含两个指针元素的数组,每个元素都指向int的指针 【指针数组】
①注意(指针变量的赋值):
int n = 5;
double x;
int * p1 = &n;
double* p2 = &x;
x = n; // 隐式类型转换
//p1 = p2; 语句会在编译时错误
②注意(const
用法)
C const 和 C++ const (用法相似)
C++允许在声明数组大小时使用const整数,C不允许
C++的指针赋值检查更严格
const int y;
const int * p2 = &y;
int * p1;
p1 = p2; // C++不允许【把const指针赋值给非const指针】,C会给出警告,但是通过p1更改y的值是未定义的
③扩展知识
变长数组(VLA)
int arr[][4]; // 可以是arr[5][4]、arr[100][4]、arr[20][4]... 即可以使用变量 改变数组的维度
// 扩展:复合字面量(匿名变量)
7 字符串相关函数
gets()、gets_s()、fgets()、puts()、puts()、fputs()
strlen()、strcat(str1, str2)
【拼接两个字符串】、strncat(str1, str2, n)
【n指定str2拼接str1的长度】
strcmp(str1, str2)
【比较两个储存在不同数组大小的字符串,大小写要一致,返回比较字符串结果的ASCII码排序,相等为0】
strncmp()
【适合查找以“moon”开头的字符串,限定只查找这4个字符】
strcpy()、strncpy()
sprintf()
【不同于上述方法(声明在string.h
中),将数据写入字符串,可以将多个元素合成一个字符串。第一个参数是目标字符串的地址,其余参数和printf()
相同】
以下面的C程序为例:
#include <stdio.h>
#define MSG "I'm a symbolic string constant."
int func_string_1(void) {
char words[81] = "I am a string in an array.";
const char* pt1 = "Pointer is pointing at me.";
puts(MSG); // puts函数只显示字符串,而且自动在显示的字符串末尾加上换行符
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
// 读取字符串时,scanf()函数和转换说明%s只能读取一个单词,gets()函数用于处理这种情况【读取整行输出,直到遇到换行符】
// 但是由于:char words[81]; 是定长的,gets()唯一的参数是words【字符串数组名】,它无法检查数组是否装得下输入行
// 若输入字符串过长,会导致缓冲区溢出(buffer overflow)-- C99建议不要使用。C11直接废除
}
gets()
函数的替代:
- 过去常用
fgets()
代替gets()
,fgets()
通过第二个参数限制读入的字符来解决溢出的问题,该函数专门用来设计处理用于文件输入
- fgets()的第二个参数指明了读入字符的最大数量,若参数为n,则读入n-1个字符,或者读到第一个换行符为止
- fgets()读到第一个换行符会把它储存在字符串中【gets()会丢弃换行符】
- fgets()的第三个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin作为参数
- 同样的。
fputs(words,stdout)
能显示输出手动输入的字符串,返回指向char的指针。【但是如果读到文件结尾,它将返回空指针】(空指针有一个地址值,该值不会与任何数据的有效地址对应。空字符【
'\0'
为字符占1字节】和空指针【地址,4字节】都可以理解为 0)
- 【C11新增的
gets_s()
也可代替】
gets_s(words, STR_LEN)
只从标准输入中读取数据,所以不需要第三个参数【文件】- gets_s()读到换行符会丢弃它
- gets_s()读到最大字符数都没有读到换行符,首先会把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或者文件结尾,然后返回空指针,接着调用依赖实现的“处理函数”,可能会中止或者退出程序
8 存储、链接
因为C是面向过程编程的语言,它的对象指的是存储值占用的内存。
指定对象的表达式称为左值
int entity = 3; // 该声明创建了一个名为entity的标识符,用来指定特定对象的内容
int a = 0; // 文件作用域(外部文件可用)
static int b = 1; // 文件(全局)作用域 static声明的是它链接属性,而非存储期
// 上面的文件作用域变量都具有静态存储期
- 关键字:
auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic
- 函数:
rand()
【伪随机数,在其取值范围内均匀分布】、srand()、time()、malloc()、calloc()、free()
- 变量的作用域和生命周期
存储期
- 静态存储期:(程序执行期间一直存在)
- 线程存储期:用于并发程序设计,从被声明到线程结束一直存在。以
_Thread_local
声明一个对象时,每个线程都获得该变量的私有备份 - 自动存储期:块作用域的变量,程序进入定义这些变量的块时为其分配内存、退出时释放内存
- 动态分配存储期
可以显式地使用
auto
表示自动变量【自动存储周期、块作用域、无链接】
寄存器变量【数据类型有限,比如可能没有足够大的空间存储double类型的值】使用
register
声明,比一般的变量(存储在内存中)更快【自动存储周期、块作用域、无链接】
块作用域的静态变量:块中使用
static
声明,静态的意思是该变量在内存中原地不动,并不是值不变
多文件【程序由多个单独的源代码文件组成】
这些文件可能要共享一个外部变量。C通过在一个文件进行定义式声明,然后在其他文件中进行引用式声明实现共享。
除了一个定义式声明(才能初始化变量)之外,其它声明都要使用extern
关键字
time()
返回值的类型为time_t
,接受的参数是time_t
类型对象地址,时间存储与在传入的地址上。
⚪5种存储类别
- 自动 【auto可省略】——自动存储期、块作用域、无链接
- 寄存器 【register】——自动存储期、块作用域、无链接,无法获取其地址
- 静态、无链接 【块中 static】——静态存储期、块作用域、无链接
- 静态、外部链接 【函数外部 无static】——静态存储期、文件(外部)作用域、外部链接
- 静态、内部链接 【函数外部 static】——静态存储期、文件(内部)作用域、内部链接
通过下面的C程序理解:
#include <stdio.h>
#include <stdlib.h> // 提供库函数rand()、malloc()、free()的原型
//#include <stdatomic.h>
int roll_count = 0;
static int rollem(int sizes) { //该文件私有函数
int roll;
roll = rand() % sizes + 1; // rand()取值范围0~RAND_MAX之间,实现掷骰子得到1~6的结果需要该行代码
++roll_count; // 函数的调用次数
return roll;
}
malloc()
函数
malloc()函数在程序运行时分配更多的内存,接受一个参数:所需的内存字节
malloc()函数会找到合适的空闲内存块,这样的内存是匿名的【分配内存但是不为其赋名】,返回动态分配内存块的首字节地址。它的返回类型通常被定义为指向char的指针。
注意:malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以它的返回值会被强制转换成匹配的类型,malloc()分配内存失败将返回空指针
从ANSI C开始,新类型:指向
void
的指针——“通用指针”
double * ptd;
ptd = (double *)malloc(30 * sizeof(double)); // 一定要手动释放
//... 代码
free(ptd);
--------------------
long * newmem;
newmem = (long *)calloc(100, sizeof(long)); // calloc接受两个无符号整数作为参数
...
free(newmem);
malloc()
与变长数组
void func_vla() { // VLA 变长数组
//int n;
//scanf("%d", &n);
//int arr[n];// 变长数组【自动存储类型】 不用free() 函数结束后自动释放
// malloc()创建二维数组
int(*p2)[6];
//int(*p3)[m]; // 需要编译器支持变长数组
//p2 = (int (*) [6])malloc(n * 6 * sizeof(int));
//p3 = (int (*) [m])malloc(n * m * sizeof(int));
int arr[2][2] = { {1,2}, {3, 4} };
int(*pi)[2]; // 它相当于一个二维数组的用法,只是它是一个n行2列的数组
// 等价于 int * pi = (int *)malloc(2 * sizeof(int));
pi = arr;
printf("%d\n", *pi[1]); // 相当于 arr[1][0]
printf("%d\n", pi[0][1]); // 相当于 arr[0][1]
printf("%d\n", (* pi + 2)[1]); // 相当于 arr[1][1] 对pi相加——某一行。中括号——某一列
}
新特性
C90新增两个属性:恒常性(constancy)和易变性(volatility)
C99新增第三个限定符:restrict
,用于提高编译器优化
C11新增第四个限定符:_Atomic
(可选支持项)以及一个可选库(stdatomic.h
——并发程序设计)
const const const int n = 6; //C99新属性 声明中多次使用同一个限定符,多余的会被忽略
/*volatile限定符,代理(而非变量所在的程序)可以改变该变量的值
通常被用于硬件地址以及其他程序或同时运行的线程中共享数据*/
volatile int locl; // locl是一个易变的位置
volatile int* ploc; // ploc是一个指向易变的位置的指针
// 使用示例:用const把硬件时钟设置为程序不能更改,但可以通过代理改变
volatile const int loc;
const volatile int* ploc;
/*restrict类型限定符允许编译器优化部分代码以更好地支持计算
它只能用于指针,表明指针是访问数据对象的唯一且初识的方式
*/
void func_restrict() {
// void ofmouth(int * const a1, int * restrict a2, int n);// old style
// void ofmouth(int a1[const], int a2[restrict], int n);// C99
int ar[10];
//int * restrict res = (int*)malloc(10 * sizeof(int)); //C99
int * par = ar;
for (int i = 0; i < 10; i++) {
par[i] += 5;
//restar[i] += 5;
ar[i] *= 2;
par[i] += 3;
//restar[i] += 3;
}
/*
因为restar是访问分配的内存的唯一且初始的方式,那么 编译器 可以将上述对restar的操作进行优化:
restar[n]+=8;
而par并不是访问 数组 ar的唯一方式,因此并不能进行下面的优化:
par[n]+=8;
因为在par[n]+=3前,ar[n]*=2进行了改变。使用了 关键字 restrict,编译器就可以放心地进行优化了。
C库中有两个函数可以从一个位置把字节复制到另一个位置。在C99标准下,它们的原型如下:
void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
void * memove(void * s1, const void * s2, size_t n);
这两个函数均从s2指向的位置复制n字节数据到s1指向的位置,且均返回s1的值。两者之间的差别由关键字restrict造成,
即memcpy()可以假定两个内存区域没有重叠。memmove()函数则不做这个假定
因此,复制过程类似于首先将所有字节复制到一个临时缓冲区,然后再复制到最终目的地。
如果两个区域存在重叠时使用memcpy()会怎样?其行为是不可预知的,既可以正常工作,也可能失败。
在不应该使用memcpy()时,编译器不会禁止使用memcpy()。因此,使用memcpy()时,必须确保没有重叠区域。
关键字restrict有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,他告诉用户仅使用满足restrict要求的参数。
*/
}
void func_atomic() {
int hogs; // 普通声明
hogs = 12; // 普通赋值
//可以替换成
//_Atomic int hogs; // hogs为一个原子类型的变量 C11
//atomic_store(&hogs, 12); // stdatomic.h中的宏
}
9 结构体
- 关键字:
struct、union、typedef
- 运算符:
.
、->
- C结构、创建结构模板和结构变量
- 访问结构程序,编写处理结构的函数
- 联合体和指向指针的函数
通过下面的程序了解:
#include <stdio.h>
#include <string.h>
#define MAXTIT 41 // 书名最大长度
#define MAXAUTH 31 // 作者名字最大长度
typedef struct book {
// book结构--传统方式打印一份图书目录【包含每本书的详细信息:书名、作者、出版社、页数、价格等】
//需要创建多个不同类型的数组,十分繁琐,使用一些特定操作【如按出版日期排序】也比较麻烦
char title[MAXTIT];
char author[MAXAUTH];
float value;
}library;
void book_struct_use() {
//struct book library; // 等价于不用typedef定义struct book的别名,在此处声明
printf("please enter the book title:\n");
/*gets(library.title, MAXTIT);*/
struct book* b; // 指向结构的指针
// 可以使用 b->value的方式告诉编译器指针指向何处
/*
带参函数 传入结构体成员【仅仅使用结构提成员的值,不改变它】
传入结构体成员地址【改变它的值】
传入结构【结构体中的成员需要初始化】
*/
}
union
是一种数据类型,它能在同一个内存空间中存储不同的数据类【不是同时存储】
典型用法:设计一种表存储既无规律、事先也不知道顺序的混合类型
union hold { // 声明的联合hold只能存储一个int或一个double或是一个char类型的值
int digit;
double bigfl;
char letter;
};
union hold fit; // 编译器分配给他足够的空间以便它能存储union声明中占用最大字节的类型【double--8byte】
union hold save[10]; // 每个元素都是分配8字节
/*用法
fit.digit = 23; //23存储在fit 占2字节
fit.bigfl = 2.0; //清除23,存储2.0 占8字节
fit.letter = 'h'; //清除2.0,存储'h' 占1字节
*/
枚举类型enum
,声明符号名称来表示整型变量【实际上enum常量是int类型,目的是提高程序的可读性】
enum spectrum { red, orange, yellow, green, blue, violet}; // 枚举了spectrum可能的值 枚举值输出的话默认为0、1、... 也可以自定义为其赋值
enum spectrum color; // 声明
if(color==yellow)...
for(color=red; color<=violet; color++)...
// 【C++的枚举变量不支持++运算符】
函数和指针
void ToUpper(char *);
void (*pf)(char *); // pf是一个指向函数的指针 可以用pf指向ToUpper函数的地址:pf = ToUpper;
void *pf(char *);//pf是一个返回字符指针的函数
10 位操作、预处理库
- 运算符:
~
【按位取反】、&
【按位与:常用于掩码】、|
【按位或】、^
【按位异或】、<<
【左移 若 二进制数<<2,则高位丢失,低位补零】、>>
【右移,有符号数和无符号数有区别】、&=、|=、^=、>>=、<<=
- 关键字:
_Alignas
【给出一个类型的对齐要求】、_Alignof
- 预处理指令:
#define
【明示常量】、#include
#undef
用于取消已定义的#define指令
#line
用于重置行和文件信息
#error
给出错误信息
#pragma
用于向编译器发出指令
#ifdef、#else、#endif、#ifndef、#if、#elif
用于指定什么情况下编写哪些代码
#ifdef MAVIS
#include "horse.h"
#define STABLES 5
#else
#include "cow.h"
#define STABLE 10
#endif
- 关键字:
_Generic、_Noreturn、_Static_assert
- 泛型选择表达式:
_Generic(x, int: 0, float: 1, double: 2, default: 3) // C11
-
函数说明符:
_Noreturn
,调用完成后函数不返回主调函数 【C11】 -
函数/宏:
sqrt()、atan()、atan2()、exit()、atexit()
【通过退出时注册被调用的函数提供这种功能,介绍一个函数指针作为参数】- 宏(macro)代表值——类对象宏;代表函数——类函数宏 【代码替换】,从宏替换成最终代码的过程称为宏展开(macro expansion)
- 示例:
#define MEAN(X,Y) (((X)+(Y))/2)
前者MEAN(X,Y)为宏,X、Y为宏参数,后者为替换体 - 预处理粘合剂:
##
运算符 如:#define XNAME(n) x ## n
宏XNAME(4)将展开为x4 - 变参宏:
...
和__VA_ARGS__
【比如printf()介绍数量可变的参数,可以通过预处理和宏定义的方式:#define PR(...) printf(__VA_ARGS__)
后面调用PR("hello");
】
-
内联函数——C99提供,起因是由于函数调用过程:建立调用、传递参数、跳转到函数代码并返回,使用宏是代码内联可以避免这样的开销
inline static void eatline(){//内联函数定义
...
}
int main(){
...
eatline();//调用
...
}
C程序:
#include <stdio.h>
// 有符号整数和无符号整数的二进制表示 原码、反码和补码
void why_me();
void pre_macro() { // C语言的一些预定义宏
printf("The file is %s.\n", __FILE__);
printf("The date is %s.\n", __DATE__);
printf("The time is %s.\n", __TIME__);
//printf("The version is %ld.\n", __STDC_VERSION__); // 支持C99:199901L、支持C11:201112L
printf("This is line %d.\n", __LINE__); // 当前源代码文件中行号的整型常量
printf("This function is %s.\n", __func__); // 预定义标识符,展开为一个代表函数名的字符串 C99
why_me();
}
void why_me() {
printf("This function is %s.\n", __func__);
printf("This is line %d.\n", __LINE__);
}
/*
数学库:math.h
泛型类型库:tgmath.h
通用工具库:stdlib.h 如rand()、srand()、malloc()、free()
断言库:assert.h assert()宏,用于辅助调试程序 C11新增:_Static_assert,在**编译时**检测assert()表达式
字符串库:string.h memcpy()、memmove()
可变参数:stdarg.h 宏:vastart()、va_arg()、va_copy()、va_end()
*/