learnc

Nov 30, 2021

C 简短过一遍

C 语言编译

gcc hello.c -o hello // gcc -std=c99 hello.c
./hello

基础知识

字符类型

char c = 'B'; // ASCII B = 66

字符类型使用一个字节(8位)存储。

'\a'(警报) '\b'(退格键) '\n'(换行符) '\r'(回车符) '\t'(制表符) '\v'(垂直分隔符) '\0'(NULL)

指针

指针

int *intPtr;

指向指针的指针

int **foo;

取出指针对应空间值

void increase(int *p) {
    *p = *p +1;
}

&运算符

int x = 1;
increase(&x);

& 运算符与 * 运算符互为逆转

int i = 5;
if (i == *(&i)) // TRUE

指针变量的初始化

声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存是随机分配的。

int *p; // 声明了指针
*p = 1; // *p 对应了某个随机地址,未知空间,此时赋值为了1的字面量

正确的做法是给与以已分配好的的地址:

int *p;
int i;
p = &i;
*p = 13;

为了防止读写未初始化的指针变量,初始化时设为NULL

int *p = NULL;

指针的运算

指针运算,表示指针的移动。

short *j;
j = (short *)0x1234;
j = j + 1; // 0x1236;

使用场景: for循环时,根据偏移,相加指针得到下一位(指针相减,得到距离位数).

函数

main 方法

参数传递,参数的传值引用

函数指针

void print(int a) {
    printf("%d\n", a);
}
void (*print_ptr)(int) = &print;

调用时如: (*print_ptr)(10); // print(10);

比较特殊的是,C 语言还规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说,print&print 是一回事。

if (print == &print) // true

因此,上面代码的print_ptr 等同于 print

void (*print_ptr)(int) = &print;
void (*print_ptr)(int) = print;
if (print_ptr == print) // true

所以,对于任意函数,都有五中调用函数的写法:

print(10)
(*print)(10)
(&print)(10)
(*print_ptr)(10)
print_ptr(10)

extern 说明符

static 说明符

const 说明符

可变参数

va_list
va_start
va_arg
va_end

printf 正是可变参数实现的示例,为什么 int(*printf_ptr)(const char* format, ...) 不需要提供给va_start 以个数?

因为printf已经在format中读取了个数。

数组

int scores[100]; // 声明数组
int a[5] = {22, 36, 38, 99, 122}; // 声明并且赋值
int a[100] = {0}; // 声明并设为0值
int a[15] = {[2] = 29, [9] = 7, [14] = 48}; // 声明并设置对应位置,其余0值
inta a[] = {22, 37, 222}; // 自动判别数组大小

数组长度

int a[] = {22, 37, 222};
int arrLen = sizeof(a); // 12

长度计算:
sizeof(a) / sizeof(a[0]); // 3

多维数组

int board[10][10];

数组的地址

int a[5] = {11, 22, 33, 44, 55};
int* p = &a[0];
int* p = a; // 等同于

数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。

已声明数组不可修改

int a[5] = { 1, 2, 3, 4, 5};
int b[5] = a; // 报错

int b[5];
b = a; // 报错

指针指向可以改,但是内存地址不可修改

数组指针的加减法

数组指针加减法其实就是内存地址之前的移动。

1
2
3
4
5
int a[5] = {11, 22, 33, 44, 55};
for (int i = 0; i < 5; i++) {
printf("%d\n", *(a+i));
}
// 例子中遍历等于数组向下一个成员内存地址移动。`*(a+i)`取出该地址的值

由于数组名与指针是等价的,所以 a[b] == *(a+b)

常使用的遍历方案:

1
2
3
4
5
6
int a[] = {11, 22, 33, 44, 55, 999};
int *p = a;
while (*p != 999) {
printf("%d\n", *p);
p++;
}

遍历数组一般都是通过数组长度比较,但也可以通过数组起始地址和结束地址比较来实现:

1
2
3
4
5
6
7
8
9
10
11
int sum(int* start, int* end) {
int total = 0;
while (start < end) {
total += *start;
start++;
}
return total;
}
int arr[5] = {20, 10, 4, 39, 4};
int arrSize = sizeof(arr) / sizeof(arr[0]);
printf("%i\n", sum(arr, arr+arrSize));

数组的复制

由于数组名是指针,不能简单复制数组名。如:

1
2
3
int* a;
int b[3] = {1 ,2, 3};
a = b;

上面写法不是复制数组,只是将a、b指针指向b原先指向的地址。

复制最简单的就是元素逐个复制:

for (i = 0; i < N; i++)
    a[i] = b[i];

另一种方法是使用memcpy()函数(include string.h):

memcpy(a, b, sizeof(b));

函数参数数组

数组名是一个指针,如果只传数组名,那么函数只知道数组开始地址,而不知道结束地址。也称之为数组降低为指针

1
2
3
4
int sum_array(int a[], int length) {
...
}
int sum = sum_array(a, 4);

多维数组,需要告知未知数组大小:

1
2
3
4
int sum_array(int a[][4], int n) {
...
}
int sum = sum_array(a, 2);

上面展示的是必须告知未知的那个数组大小。

变长数组作为参数

1
2
3
4
int sum_array(int n, int a[n]) {
...
}
int sum = sum_array(n, a);

变长数组中,n作为参数必须放在a[n]前,这样运行时才能确定数组a[n]的长度。

int sum_array(int a[][4], int n);
int sum_array(int n, int m, int a[n][m]);

数组字面量作为参数

int a[] = {2, 3, 4, 5};
int sum = sum_array(a, 4);

int sum = sum_array((int []){2, 3, 4, 5}, 4);

字符串

C语言没有单独的字符串类型。字符串即字符数组。

char *word = { ‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’ }; // char word[] = “Hello”;

字符末尾必须添加\0(ANSII NULL) 表示字符结束,这样只要发现\0,那么就知道字符串结束了。

char localString[10];

上面示例声明了一个10个字符的字符数组。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。

字符串的声明

char s[50] = “hello”; // s长度,其余为 \0

字符数组的长度,不能小于字符串的实际长度:

char s[5] = “hello”;

上面示例中,字符串数组s的长度是5,小于字符串”hello”的实际长度6。

编译时不报错,实际只会有5个字符写入到内存中,再长也是只有5个字符。

char s[5] = “hello!”;

此时编译器会Warning,并且会继续往后打印,直到出现\0.

原因是编译期间,代码识别为5的长度,后面也只会申请5个单位长度的字符数组大小。

有趣的是,char s[5] 在申请完内存空间后,会单独再申请一个int 为5的内存空间。

char* s = "Hello, world";
s[0] = 'z'; // 错误

char* s1 = "Hello, world";
s[0] = 'z'; // 成功

指针声明的数组,系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入到这段新分配的内存中,这段内存是允许修改的。

字符串修改

char* s = "Hello";
s = "World"; // 正确,指针指向不同内存地址肯定可以

char s[] = "hello";
s = "world"; // 报错

指针指向另一块内存空间,这个好理解,肯定OK。但是数组的=相当于赋值,C语言规定,数组变量是一个不可修改的左值,即不能用赋值运算符为他重新赋值。

正确赋值方式:

char s[10];
strcpy(s, "abc");

strlen

注意strlen 和sizeof 是两个不同的概念:

char s[50] = "hello";
printf("%d\n", strlen(s)); // 5
printf("%d\n", sizeof(s)); // 50

strcpy

char* (*strcpy)(char dest[], const char source[]);

1
2
3
4
5
6
7
8
char* strcpy(char* dest, const char* src) {
while (*dest++ = *source++);
return dest;
}
int main() {
char str[20];
strcpy(str, "hello, world");
}

strncpy

char* (*strncpy)(char*dest, char* src, size_t n);

strcat

char* (*strcat)(char* s1, const char* s2);

strncat

char* (*strncat)(char* dest, const char*src, size_t n);

strncat(str1, str2, sizeof(str1) - strlen(str1) - 1); // 平常用法

n 代表str1 最大还可拼接单位: str1 内存空间 - str1 已占用长度 -1 就是后面还可拼接长度。(-1 代表 \0: strlen 不算\0)

strcmp

int (*strcmp)(const char* s1, const char* s2);

strncmp

int (*strncmp)(const char* s1, const char* s2, size_t n);

char s1[12] = "hello world";
char s2[12] = "hello C";
int status = strncmp(s1, s2, 5); // 0 相同

sprintf, snprintf

int (*sprintf)(char* s, const char* format, ...);

char first[6] = "hello";
char last[6] = "world";
char s[40];
sprintf(s, "%s %s", first, last); // s = hello world

int (*snprintf)(char* s, size_t n, const char* format, ...);

snprintf(s, 4, "%s %s", "hello", "world"); // hel

内存

栈: 系统管理的内存,存放在栈上;堆heap: 用户手动创建,用户手动管理。

void 指针

int x = 10;
void* p = &x;
int* q = p;

malloc

void* (*malloc)(size_t size)

malloc函数用于分配内存,该函数向系统要求一段内存,系统就在”堆”里面分配一段连续的内存块。原型定义在头文件stdlib.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
char* a = malloc(sizeof(char)*50);
puts(a);
printf("111----%p\n", a);
printf("111====%s\n", a);
strcpy(a, "我喜欢小钮");
printf("222----%p\n", a);
printf("222====%s\n", a);
char arr[] = "我喜欢小可爱";
for (int i = 0; i < 20; i++) {
a[i] = arr[i];
}
printf("333----%p\n", a);
printf("333====%s\n", a);
}

数组char a 不能被直接 `a = “新的数组”`, 应该采用上面两种方式来内存写入。

C语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。

free

void (*free)(void* block)

free用于释放malloc函数分配的内存,将这块内存还给系统以便重新使用,否则这个内存块会一直占用到程序运行结束。

int* p = (int*)malloc(sizeof(int));
*p = 12;
free(p);

calloc

void* (*calloc)(size_t n, size_t size)

calloc函数的作用与malloc相似,也是分配内存块。

区别亮点

  1. calloc接受两个参数,第一个参数是某种数据类型的值的数量,第二个是该数据类型的单位字节长度。
  2. calloc会将所分配的内存全部初始化为0malloc不会对内存进行初始化,如果想要改为0值,则额外调用memset()函数。

    int* p = calloc(10, sizeof(int));
    // 等同于
    int* p = malloc(sizeof(int)*10);
    memset(p, 0, sizeof(int)*10);
    

calloc分配的内存块,也要使用free释放。

realloc

void* (*realloc)(void* block, size_t size)

realloc函数用于修改已经分配的内存块的大小,放大或者缩小,返回一个指向新的内存块的指针。如果分配不成功,返回NULL。

int* b;
b = malloc(sizeof(int)*10);
b = realloc(b, sizeof(int)*2000);

restrict说明符

声明指针变量时,可以使用restrict说明符,告诉编译器该块内存区域只有当前指针一种访问方式,其他指针不能读写该块内存。这种指针成为”受限指针”(restrict pointer)。

int* restrict p;
p = malloc(sizeof(int));

memcpy

void* (*memcpy)(void* restrict dest, void* restrict source, size_t n)

1
2
3
4
5
6
int main(void) {
char s[] = "Goats!";
char t[100];
memcpy(t, s, sizeof(s)); // 拷贝7个字节,包括终止符
printf("%s\n", t);
}

memcpy可以取代strcpy 进行字符串拷贝,而且是更好的方法,不仅更安全,速度也更快,它不检查字符串尾部的\0字符。

char* s = "hello world";
size_t len = strlen(s) + 1;
char* c = malloc(len);

if (c) {
    // strcpy 写法
    strcpy(c, s);
    // memcpy 写法
    memcpy(c, s, len);
}

使用void指针,也可以自定义一个复制内存的函数:

1
2
3
4
5
6
void* my_memcpy(void* dest, void* src, int byte_count) {
while (byte_count--) {
*dest++ = *src++;
}
return dest;
}

memmove

void* (*memmove)(void* dest, void* source, size_t n)

memmove函数用于将一段内存数据复制到另一段内存。它跟memcpy主要区别是,它允许目标区域与原区域有重叠。如果重叠,原区域内容会被更改。

int a[100];
memmove(&a[0], &a[1], 99*sizeof(int));

另一个例子:

char x[] = "Home Sweet Home";
// 输出Sweet Home Home
printf("%s\n", (char*) memmove(x, &x[5], 10));

memcpy

int (*memcmp)(const void* s1, const void* s2, size_t n)

char* s1 = "abc";
char* s2 = "acd";
int r = memcmp(s1, s2, 3);

struct 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct fraction {
int numerator;
int denominator;
};
struct fraction f1;
f1.numerator = 22;
f1.denominator = 7;

struct Car {
char* name;
float price;
int speed;
};
struct Car saturn = {"Saturn SL/2", 16000.00, 175}; // struct Car saturn = {.speed=172, .name="Saturn SL/2"};
saturn.speed = 168;

struct {
char title[500];
char author[100];
float value;
} b1 = {"Harry Potter", "J. K. Rowling", 10.0},
b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};

`typedef` 为struct结构指定一个别名:

typedef struct cell_phone {
int cell_no;
float minutes_of_charge;
} phone;

phone p = {5551234, 5};

struct 的复制

struct 变量的使用复制运算符=,复制给另一个变量,这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过来,即原样生成了一份数据。

1
2
3
4
5
6
7
8
9
10
struct cat { char name[30]; short age; } a, b;

strcpy(a.name, "Hula");
a.age = 3;

b = a;
b.name[0] = "M";

printf("%s\n", a.name); // Hula
printf("%s\n", b.name); // Mula

上面示例是有前提的,就是struct 结构的属性必须定义成字符数组,才能复制数据。如果指针数组,只是指针的复制,内存地址指向相同地址:

struct cat { char* name; short age; } a, b;

struct 指针

struct 变量传入函数,类似 = 的对象拷贝,传值操作时:

1
2
3
4
void happy(struct turtle* t) {
(*.t).age = (*t).age + 1;
}
happy(&myTurtle);

(*t).age 这样的写法很麻烦。C语言引入了一个新的箭头运算符(->),可以从struct指针上直接获取属性,大大增强了代码的可读性。

t->.age = t->age + 1;

struct 位字段

struct 弹性数组成员

1
2
3
4
5
6
7
struct vstring {
int len;
char chars[];
};
struct vstring* str = malloc(sizeof(struct vstring)+n*sizeof(char));
str->len = n;
str->chars = "我喜欢吃饭";

typedef命令

typedef命令用来为某个类型起别名。typedef type name;

typedef unsigned char BYTE;
BYTE c= 'z';

typedef 可以为指针起别名:

typedef int* intptr;
int a = 10;
intptr x = &a;

typedef也可以用来为数组类型起别名:

typydef int five_ints[5];
five_ints x = {11, 22, 33, 44, 55};

typedef 为函数起别名:

typedef signed char (*fp)(void); // 类型别名fp是一个指针,代表函数 signed char(*)(void)

Union 结构

Enum类型

enum colors {RED, GREEN, BLUE};

预处理器(Preprocessor)

#define

1
2
3
4
5
6
#define PRINT_NUMS_TO_PRODUCT(a, b) { \
int product = (a) * (b); \
for (int i = 0; i < product; i++) { \
printf("%d\n", i); \
} \
}

#运算符, ##运算符

不定参数的宏

1
2
3
4
#define X(a, b, ...) (10*(a) + 20*(b), __VA_ARGS__)
X(5, 4, 3.14, "Hi!", 12)
// 替换成:
(10*(5)+20*(4)), 3.14, "Hi!", 12
#define X(...) #_VA_ARGS__
printf("%s\n", X(1, 2, 3)); // "1, 2, 3"

#undef

#undef指令用来取消已经使用#define定义的宏。

#define LIMIT 400
#undef LIMIT

#include

#if ...#endif

1
2
3
4
5
6
7
8
9
#define FOO 1
#if FOO
const double pi = 3.1415;
printf("define\n");
#elif FOO == 2
printf("never enter!\n");
#else
printf("not define\n");
#endif

gcc -DDEBUG=1 foo.c 可以在编译期间指定宏:

#if DEBUG
// enter
#endif

#ifdef...#endif

1
2
3
4
5
#ifdef EXTRA_HEPPY
printf("I'm extra happy!\n");
#else
printf("I'm just regular\n");
#endif

defined 运算符

#ifdef 指令,等同于#if defined

1
2
3
#ifdef FOO
// 等同于
#if defined FOO

#ifndef…#endif

#ifndef 等同于 #if !defined

预定义宏

#line

#error

#error 指令用于让预处理器抛出一个错误, 终止编译。

1
2
3
#if __STDC_VERSION__ != 201112L
#error Not C11
#endif

如果编译器不适用C11标准,就终止编译。GCC编译器会想下面这样报错:

$ gcc -std=c99 newish.c
newish.c:14.2: error: #error Not C11

#pragma

I/O 函数

缓存和字节流

printf

scanf

int i, j;
float x, y;
scanf("%d%d%f%f", &i, &j, &x, &y);

scanf()返回值是一个整数,表示成功读取的变量的个数。

scanf() 读取%s 字符串时需要注意数组溢出风险:

char name[11];
scanf("%10s", name);

赋值忽略符

有时用户舒服可能不符合预期格式:

scanf("%d-%d-%d", &year, &month, &day);

上面示例用户输入2020-01-01, 就会正确读出年月日。问题用户可能输入其他格式: 2020/01/01

为了避免上述情况,scanf() 提供一个赋值忽略符*. 只要把*加到任何占位符的百分号后面,解析就会被抛弃:

scanf("%d%*c%d%*c%d", &year, &month, &day);

sscanf

sscanf从字符串读取,而不是如scanf从用户输入读取。

fgets(str, sizeof(str), stdin);
sscanf(str, "%d%d", &i, &j);

getchar / putchar

char ch;
ch = getChar();
// 等同于
scanf("%c", &ch);

例子, 计算某一行的字符长度:

1
2
3
int len = 0;
while(getchar() != '\n')
len++;
putchar(ch);
// 等同于
printf("%c", ch);

gets / puts

文件操作

FILE* fp;

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(void) {
FILE* fp;
char c;
fp = fopen("hello.txt", "r");
if (fp == NULL) {
return -1;
}
c = fgetc(fp);
printf("%c\n", c);
fclose(fp);
return 0;
}

fopen()打开指定文件,返回一个FILE指针。它相当于将指定文件的信息与新建的文件指针fp相关联,在FILE结构内部记录了这样一些信息: 文件内部的当前读写位置、读写报错的记录、文件结尾指示器、缓冲区开始位置的指针、文件标识符、一个计数器(统计拷贝进缓冲区的字节数)等等。同时,它还为文件建立一个缓冲区。由于存在缓冲区,也可以说fopen()函数打开一个流,后继读写文件操作都是流模式。

fgetc() 已调用,文件的数据块先拷贝到缓冲区。不同的计算机有不同的缓冲区大小,一般是512字节或是它的背书,如4096或16384.

fgetc()从缓冲区读取数据,同时将文件指针内部的读写位置指示器,指向所读取字符的下一个字符。所有的文件读取函数都使用相同的缓冲区,后面再调用任何一个读取函数,都将从指示器指向的位置,即上一次读取函数停止的位置开始读取。

当读取函数发现已读完缓冲区里面的所有字符时,会请求把下一个缓冲区大小的数据块,从文件拷贝到缓冲区中。读取函数就以这种方式,读完文件的所有内容,知道文件结尾。

fopen

FILE* (*fopen)(char* filename, char* mode);

fp = fopen("in.dat", "r");
if (fp == NULL) {
    printf("Can't open file!\n");
    exit(EXIT_FAILURE);
}

fopen()函数会为打开的文件创建一个缓存区。读模式下,创建的是读缓冲区;写模式下,创建的是写缓冲区;读写模式下,会同时创建两个缓冲区。C语言通过缓冲区,已流的形式,向文件读写数据。

fopen()的模式字符串,默认是以文本流读写。如果添加b后缀(表示binary),就会以”二进制流”进行读写。比如, rb是读取二进制数据模式,wb是写入二进制数据模式。

mode 模式有: r w a r+ w+ a+, 还有x后缀,表示独占模式(exclusive)。如果文件已经存在,则打开文件失败;如果文件不存在,则新建文件,打开后不再允许其他程序或线程访问当前文件。比如, wx表示以独占模式写入文件,如果文件已经存在,就会打开失败。

标准流

stdin 0
stdout 1
stderr 2

如果标准输入不绑定键盘,而是绑定其他文件,可以再文件名前面加上<, 跟在程序名后面。这叫做”输入重定向”(input redirection).

demo < in.dat

上面示例中,demo程序代码里面的stdin, 将指向文件in.dat, 即从in.dat获取数据。

如果标准输出绑定其他文件,而不是显示器,可以在文件名前加上>, 跟在程序名后面。这叫做”输出重定向”(output redirection).

demo > out.dat

输出重定向>会先擦去out.dat的所有原有的内容,然后再写入。如果希望写入的信息追加在out.dat的结尾,可以使用>>符号。

demo >> out.dat

标准错误的重定向符号是 2>。其中的2代表文件指针的编号。

各种重定向和合并在一条命令里面:

demo < in.dat > out.dat 2> err.txt

重定向还有另一种情况,就是将一个程序的标准输出stdout,指向另一个程序的标准输入stdin,这时要使用|符号:

random | sum

fclose

EOF

freopen

FILE* (*freopen)(char* filename, char* mode, FILE stream);

freopen("output.txt", "w", stdout);
printf("helloc");

下面是freopen关联scanf例子

int i, i2;
scanf("%d", &i);
freopen("someints.txt", "r", stdin);
scanf("%d", &i2);

fgetc / getc

1
2
int (*fgetc)(FILE* stream);
int (*getc)(FILE* stream);

getc一般用宏实现,而fgetc 一般是函数实现;返回值的类型是int,因为读取失败的情况下,他们会返回EOF, 这个值一般是-1.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
FILE* fp;
fp = fopen("hello.txt", "r");
int c;
while ((c = getc(fp)) != EOF)
printf("%c", c);
fclose(fp);
}

fputc / putc

1
2
int (*fputc)(int char, FILE* stream);
int (*putc)(int char, FILE* stream);

fprintf

int (*fprintf)(FILE* stream, const char* format, ...);

fprintf可以替代printf:

printf("hello, world\n");
fprintf(stdout, "hello, world\n");

fscanf

int (*fscanf)(FILE* stream, const char* format, ...);

fscanf(fp, "%d%d", &i, &j);

fscanf可以连续读取,知道读到文件尾,或者发生错误(读取失败、匹配失败),才会停止读取,所以fscanf通常放在循环里面:

while(fscanf(fp, "%s", words) == 1)
    puts(words);

fgets

char* fgets(char* str, int STRLEN, FILE* fp);

fgets读取STRLEN -1个字符后,获取遇到换行符与文件结尾,就会停止读取,然后再已经读取的内容末尾添加一个空字符\0,使之成为一个字符串。注意, fgets会将换行符(\n)存储进字符串。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
FILE* fp;
char s[1024]; // 数组必须足够大,足以放下一行
int linecount = 0;

fp = fopen("hello.txt", "r");
while (fgets(s, sizeof s, fp) != NULL)
printf("%d: %s", ++linecount, s);

fclose(fp);
}

循环读取用户的输入:

1
2
3
4
5
6
7
8
char words[10];
puts("Enter strings (q to quit):");
while (fgets(words, 10, stdin) != NULL) {
if (words[0] == 'q' && words[1] == '\n')
break;
puts(words);
}
puts("Done.");

fputs

fputs函数用于向文件写入字符串,和puts函数只有一点不同,那就是它不会在字符串末尾添加换行符。这是因为fgets保留了换行符,所以fputs就不添加了。fputs函数通常与fgets配对使用。

int (*fputs)(const char* str, FILE* stream);

示例:

char words[14];
puts("Enter a string, please.");
fgets(words, 14, stdin);

puts("This is your string:");
fputs(words, stdout);

fwrite

fwrite用来一次性写入较大的数据块,主要用途是将数组数据一次性写入文件,适合写入二进制数据。

1
2
3
4
5
6
size_t (*fwrite)(
const void* ptr, // 无类型指针
size_t size, // 成员大小
size_t nmemb, // 成员数量
FILE* fp
);

要将整个数组arr 写入文件,可以采用下面的写法:

fwrite(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), fp);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
typedef struct tperson {
char name[200];
int height;
char nickname[100];
struct tperson* next;
} person;
void write() {
person p = {"akerdi", 180, "900m"};
person np = {"littleshuai", 190, "-900m"};
p.next = &np;
FILE* fp;
fp = fopen("p.txt", "wb");
fwrite(&p.name, sizeof(p.name), 1, fp); // 大小、数量互换是相同的
fwrite(&p.nickname, sizeof(p.nickname), 1, fp);
fwrite(&p.height, sizeof(p.height), 1, fp);
fwrite(&p.next->name, sizeof(p.next->name), 1, fp);
fwrite(&p.next->nickname, sizeof(p.next->nickname), 1, fp);
fwrite(&p.next->height, sizeof(p.next->height), 1, fp);

fclose(fp);
}
void read() {
FILE* fp = fopen("p.txt", "rb");
person p = {};
person np = {};
p.next = &np;
fread(&p.name, sizeof(p.name), 1, fp);
fread(&p.nickname, sizeof(p.nickname), 1, fp);
fread(&p.height, sizeof(p.height), 1, fp);
fread(&p.next->name, sizeof(p.next->name), 1, fp);
fread(&p.next->nickname, sizeof(p.next->nickname), 1, fp);
fread(&p.next->height, sizeof(p.next->height), 1, fp);
printf("%s\n",p.name);
printf("%s\n",p.nickname);
printf("%d\n",p.height);
printf("%d\n",p.next->height);
printf("%s\n",p.next->name);
printf("%s\n",p.next->nickname);

fclose(fp);
}
int main() {
write();
read();
}

👍🏻好示例

fread

1
2
3
4
5
6
size_t (*fread)(
void* ptr, // 目标指针
size_t size, // 成员大小
size_t nmemb, // 成员数量
FILE* fp
);
double earnings[10];
fread(earnings, sizeof(double), 10, fp);

feof

feof 函数判断文件的内部指针是否指向文件结尾。

int (*feof)(FILE* fp);

1
2
3
4
5
6
7
8
int num;
char name[50];
FILE* cfPtr = fopen("clients.txt", "r");
while (!feof(cfPtr)) {
fscanf(cfPtr, "%d%s\n", &num, name);
printf("%d %s\n", num, name);
}
fclose(cfPtr);

feof()为真时,可以通过fseek()rewind()fsetpos()函数改变文件内部读写位置的指示器,从而清除这个函数的状态。

fseek

每个文件指针都有一个内部指示器(内部指针),记录当前打开的文件的读写位置(file position),即下一次读写从哪里开始。文件操作函数(比如getc()fgets()fscanf()fread()等)都从这个指示器指定的位置开始按顺序读写文件。

如果希望改变这个指示器,将它移到文件的指定位置,可以使用fseek()函数。

1
2
3
4
5
int (*fseek)(
FILE* stream,
long int offset, // 正值(向文件末尾移动)、负值(向文件开始处移动)或0(不动)
int whence // 位置基准,用来确定计算起点。它的值是三个宏(`stdio.h`): `SEEK_SET(文件开始处)`、`SEEK_CUR(内部指针的当前位置)`、`SEEK_END(文件末尾)`
);
1
2
3
4
5
fseek(fp, 0L, SEEK_SET);
fseek(fp, 0L, SEEK_END);
fseek(fp, 2L, SEEK_CUR);
fseek(fp, 10L, SEEK_SET);
fseek(fp, -10L, SEEK_END);
for (count = 1L; count <= size; count++) {
    fseek(fp, -count, SEEK_END);
    ch = getc(fp);
}

fseek() 最好只用来操作二进制文件,不要用来读取文本文件。因为文本文件的字符有不同的编码,某个位置的准确字节位置不容易确定。

ftell

ftell() 函数返回文件内部指示器的当前位置。

long int (*ftell)(FILE* stream);

ftell()可以跟fseek() 配合使用,先记录内部指针的位置,操作之后返回原来的位置。

long file_pos = ftell(fp);
// ...
fseek(fp, file_pos, SEEK_SET);

先将指示器定位到文件结尾,然后得到文件开始处到结尾的字节数:

fseek(fp, 0L, SEEK_END);
long size = ftell(fp);

rewind

void (*rewind)(FILE* stream);

rewind(fp)基本等价于fseek(fp, 0L, SEEK_SET);, 唯一区别是rewind()没有返回值,而且会清除当前文件的错误指示器。

fgetpos() / fsetpos()

1
2
int (*fgetpos)(FILE* stream, fpos_t* pos);
int (*fsetpos)(FILE* stream, const fpos_t* pos);

fgetpos() / fsetpos() 相当于位数比fseek()/ftell()的功能。long int 长度为4字节,能够表示范围最大为4GB,于是就有了这两个函数。

fpos_t file_pos;
fgetpos(fp, &file_pos);
// ...
fsetpos(fp, &file_pos);

ferror() / clearerr()

1
2
int (*ferror)(FILE* stream);
void (*clearerr)(FILE* fp);

ferror() 函数用来返回错误指示器的状态。可以通过这个函数,判断前面的文件操作是否成功。

clearerr() 函数用来重置出错指示器。

FILE* fp = fopen("file.txt", "w");
char c = fgetc(fp);
if (ferror(fp)) {
    printf("读取文件: file.txt 时发生错误\n");
}
clearerr(fp);
1
2
3
4
5
6
7
8
9
10
11
if (fscanf(fp, "%d", &n) != 1) {
if (ferror(fp)) {
printf("io error\n");
}
if (feof(fp)) {
printf("end of file\n");
}

clearerr(fp);
fclose(fp);
}

remove

remove()函数用于删除指定文件。正确返回0值。

int (*remove)(const char* filename);

remove("foo.txt");

注意,删除文件必须是在文件关闭的状态下。如果使用fopen()打开的文件,必须先用fclose()关闭后删除。

rename

int (*rename)(const char* old_filename, const char* new_filename);

变量说明符

更多

const

static

auto

extern

register

register说明符向编译器表示,该变量经常使用,应该提供最快的读取速度,所以应该放进寄存器。

register int a;

register 只对声明在代码块内部的变量有效。

register int a;
int *p = &a; // 编译器报错(设为register 的变量,不能获取它的地址)

volatile

restrict

restrict说明符允许编译器优化某些代码。它只能用于指针,表明该指针是访问数据的唯一方式

int* restrict pt = (int*)malloc(10*sizeof(int));

多文件项目

命令行环境

命令行参数

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; i++) {
        printf("arg %d: %s\n", i, argv[i]);
    }
}

退出状态

echo $?

环境变量

int main() {
    char* val = getenv("HOME");
}

多字节字符

Unicode简介

字符的表示方式

  • \123: 以八进制值表示一个字符,斜杠后面需要三个数字。
  • \x4D: 以十六进制表示一个字符,\x后面是十六进制证书。
  • \u2620: 以Unicode码点表示一个字符,码点以十六进制表示,\u后面需要4字符。
  • \U0001243F: 以Unicode码点表示一个字符,\U后面需要8个字符。

    printf("ABC\n");
    printf("\101\102\103\n");
    printf("\x41\x42\x43\n");
    
    上面三行都会打印"ABC".
    
    printf("\u2022 Bullet 1\n");
    printf("\U00002022 Bullet 1\n");
    
    上面两行都会打印"• Bullet 1".
    

多字节字符的表示

宽字节

所谓”宽字符”,就是每个字符占用的字节数是固定的,要么是2个字节,要么是4个字节。

宽字符有一个单独的数据类型wchar_t,每个宽字符都是这个类型。它属于整数类型的别名,可能是有符号的,也可能是无符号。

宽字符的字面量必须加上前缀”L”, 否则C语言会把字面量当做窄字符类型字符。

1
2
3
4
5
6
7
setLocale(LC_ALL, "");

wchar_t c = L'牛';
printf("%lc\n", c);

wchar_t* s = L"春天";
printf("%ls\n", s);

多字节字符处理函数

mblen()

int (*mblen)(const char* mbstr, size_t n);

他返回该字符占用的字节数。

setLocale(LC_ALL, "");
char* mbs1 = "春天";
printf("%d\n", mblen(mbs1, MB_CUR_MAX)); // 3

char* mbs2 = "abc";
printf("%d\n", mblen(mbs2, MB_CUR_MAX)); // 1
wctomb()

wctomb函数(wide charactor to multibyte) 用于将宽字符转为多字节字符。

int (wctomb)(char* s, wchar_t wc);

setLocale(LC_ALL, "");

wchar_t wc = L'牛';
char mbStr[10] = "";

int bBytes = 0;
nBytes = wctomb(mbStr, wc);
// mbStr 牛
// nBytes 3
mbtowc()

mbtowc() 用于将多字节字符转为宽字符。

int (*mbtowc)(wchar_t* wchar, const char* mbchar, size_t count);

setLocale(LC_ALL, "");

char* mbchar = "牛";
wchar_t wc;
wchar_t* pwd = &wc;

int nBytes = 0;
bBytes = mbtowc(pwc, mbchar, 3);
// nBytes 3
// *pwc ''牛
wcstombs()

wcstombs()用来将宽字符串转换为多字节字符串。

size_t (*wcstombs)(char* mbstr, const wchar_t* wcstr, size_t count);

setLocale(LC_ALL, "");

char mbs[20];
wchar_t* wcs = L"春天";

int nBytes = 0;
nBytes = wcstombs(mbs, wcs, 20);
// mbs 春天
// nBytes 6

如果wcstombs()的第一个参数是NULL,则返回转换成功所需要的目标字符串的字节数。

mbstowcs()

mbstowcs()用来将多字节字符串转换为宽字符串。

size_t (*mbstowcs)(wchar_t* wcstr, const char* mbstr, size_t count);

setLocale(LC_ALL, "");

char* mbs = "天气不错";
wchar_t wcs[20];

int nBytes = 0;
nBytes = mbstowcs(wcs, mbs, 20);
// wcs L"天气不错"
// nBytes 4

完全参考

请直接跳转阮老师博文

阮一峰clang