跳转至

Warning

未完工!等我学完CS61A后再接着写吧QAQ。

EVERYTHING begins with C

约 1171 个字 175 行代码 预计阅读时间 6 分钟

Chapter 0:Preface

0.1 初衷

这个笔记建立的初衷是帮助笔者深刻记忆C语言的语法和特性,也作为加深对C语言的理解的工具(似乎终极目的就是提升程算分数编程能力)使用。

C语言因为其比较贴近底层,语法精简而高效,扩展性和可移植性强而闻名,因而作为计算机专业学生的第一门语言存在。另外,笔者在学习完C后,学习Python的过程极其愉悦()

Chapter 1 Basis

常见数据类型

字面量即一个值:

  • 整型:123表示十进制的123;0123表示八进制的123,亦即十进制的83;0x123是十六进制的123,亦即十进制的291。

  • 字符型:

  • 一个字符类型相当于一个字节的整型,所以字符类型可以通过整型来表示:char c = 65

  • 引号里的反斜杠\有转义的效果,比如'\n'表示一个控制字符,而反斜杠只有通过\\才能表示出来。
  • 反斜杠后边可以接最多三个数字,并且此时使用八进制表示一个字节,且遇见0~7之外的数字就会阶数当前字节,比如'\101'表示A,而'08'由于8超过了八进制的范围,这就是两个字符放在了一个单引号里边,是错误的用法,如果写成字符串,"\08"就表示两个字符:一个空字符和一个8
  • \x后边接在0-9A-F内的字符,可以通过十六进制表示一个字符,不过没有长度限制,遇到范围外的字符就结束,比如\x000041也是一个字符。

字节分配:char1byte,short2byte,int4byte,long 4byte,long long8byte, float4byte,double8byte,pointer4/8byte。

运算符优先级

  • 优先级最高的:(Left-to-Right)后缀运算符:后缀形式的递增递减、函数调用、数组下标、访问结构、复合字面量。
  • 优先级略低的:(Right-to-Left)单目运算符:前缀形式的递增递减、单目的正负、逻辑非和按位非、强制类型转换、解引用、取地址、sizeof、对齐。
  • 算数运算:(Left-to-Right)乘除取余、加减、移位——优先级递减。
  • 关系运算:(Left-to-Right)不等关系、相等关系。
  • 位运算:(Left-to-Right)按位与、异或、或。
  • 逻辑运算:(Left-to-Right)逻辑与、逻辑或。
  • 条件运算:(Right-to-Left)三目运算符?:
  • 赋值运算:(Right-to-Left)各种赋值,包括复合的赋值运算。
  • 逗号运算符:(Left-to-Right),

% & <<不能用在double\float上。

printf()的转换说明修饰符

const

Chapter 3 函数

3.2 内联函数

Chapter 4 数组和指针

重要:声明一个指针只会分配一个给指针变量的空间(这部分空间用来存储它指向的位置的地址值),而不会分配指向的空间。使一个指针可用可以将其它变量取地址赋值给它,这样它指向的位置就是有效的。或者通过 malloc来新分配一块堆上的内存,malloc 的返回值就是这块内存的首地址,也是你可用的。

:二维数组不能退化为二级指针;数组名不能被重新赋值。

:数组是数组,指针是指针(这里指类型)。

:指针相减的意义是计算两个指针相差几个“单位”的距离,而不是将其值简单的相减。比如:

  • c int a[] = {1, 2, 3, 4, 5}; int *p = a, *q = &a[2]; printf("%lu", q-p); // Output: 2

  • c double a[]={1, 2, 3, 4, 5}; printf("%d", (int)&a[3] - (int)&a[0]); // Output: 24

神坑:变长数组不能通过int a[n] = {0};的方式初始化

Chapter 5:字符串和字符串函数

5.1 字符串的定义与初始化

字符串其实是以空字符\0结尾的char类型数组,因此,我们可以像处理一般数组的方式处理字符串,比如:

1
2
3
char words[81] = "I am a string in an array.";//定义字符串words
words[8] = 'p';//将字符串的第9个字符改为'p'
const char* MSG = "This cannot be changed";//定义只读字符串MSG

如果要打印MSG[22],则输出的是空字符,空字符不是空格,不会在输出窗口占用位置,只是标志字符串数组的结束。

我们一般用三种方法定义字符串:字符串常量、char类型数组、指向char类型的指针。被双引号括起来的内容被视为指向该字符串存储位置的指针,这类似于将数组名作为指向该数组的指针。比如以下程序:

printf("%s,%p,%c","We","are","champions");
//Output: We,0x10000f61,c

我们对字符串用%c%p进行转换的时候,转换过去的其实是字符串第一个元素的地址和其对应的字符

数组形式的字符串(如char arr1[] = "III" )在计算机的内存中分配一个内含4个元素的数组,每个元素作为一个字符,且最后一个元素为空字符。先将字符串常量存储在静态存储区中,程序开始运行之后为数组分配内存,初始化数组将静态存储区的字符串拷贝到数组中,编译器将数组名arr1作为该数组首元素地址的别名,而且作为地址常量,不能被改变。

一般来说,指针形式的定义一般于字符串字面量一起使用,被双引号括起来的内容是字符串字面量,而且被视为字符串的地址。指针形式(如char *pt1 = "III"让编译器在静态存储区中分配4个元素的空间,开始运行程序时,编译器为指针变量(*pt1)留出一个存储位置,该变量最初指向该字符串的首字母,但是它的值可以被改变,即可以使用递增运算符。

由于指针形式字符串的存储形式,一般建议将指针初始化为字符串自变量时使用const 限定符。

编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量,所以下面程序打印出来的都是"Jey"

1
2
3
4
char *p1 = "Hey";
p1[0] = 'J';
printf("Hey");
printf("%s","Hey");

由于数组名是一个指针变量,所以不能用str1 = str2 来简单地拷贝数组,这样只会让两个指针指向相同的内存区域。

我们可以定义字符串数组,也就是通过数组下标来访问不多个不同的字符串,有两种方式:使用存储字符串指针的数组或者多维数组:

const char* strarr1[3] = {
    "Hello",
    "Pardon",
    "Excuse me";
};
char strarr2[3][10] = {
    "Hello",
    "Pardon",
    "Excuse me";
};

这两种方式最后实现的效果是几乎一样的,都代表着五个字符串,只使用一个下标时只代表一个字符串。比如strarr1[0]strarr2[0]都代表着字符串 "Hello"

一般来说对数组的操作都是依赖于指针进行的。

5.2 字符串输入和输出

5.2.1 分配空间

最简单的分配空间的方式就是在 stack 上建立数组变量,而且还只能如此建立

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

再就是利用C库函数malloc()分配内存,比如char *name = (char *) malloc (sizeof(char)*8)这样就可以按照数组形式的字符串来使用字符串了

5.2.2 危险的gets()函数

C11标准中,废弃了不安全的gets()函数,但是大多数编译器为了兼容性,仍然保留gets()函数。

gets()函数读取一整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符在传递进来的字符串指针指向的地址上,并在字符的末尾添加一个空字符,使其成为字符串。比如:

1
2
3
4
5
int strl = 10;
char words[strl];
puts("Enter a string please.");
gets(words);
puts(words):

puts()函数经常和gets()函数一起使用,这个函数用于显示字符串,并且在字符串的末尾添加换行符。

使用gets()函数时,gets()函数只知道数组的开始处,而不会检查数组的长度和字符串的长度是否相融洽。如果输入的字符串过长,超出了数组的存储范围,就会造成缓冲区溢出(buffer overflow),读取的数据将一直向后存储,覆盖掉后边内存上的内容,如果这些多余的字符只是占用了未被使用的内存,就不会立刻出现问题,而如果擦写掉了程序中的其余内存,这样就会让程序异常终止,或者出现其他情况。

出现fragmentation fault的错误的时候,一般是程序试图访问某些未被分配的内存。

5.2.3 gets()的替代品

  • fgets()fputs()函数

fgets()函数接受三个参数:字符串存储的位置、读入字符的最大数量和要输入的文件。

fgets()函数接受的第二个参数时读入数组的最大数量,如果该参数的值是n那么fgets()将读入n-1个字符,并且在最后加上一个空字符,或者读到第一个换行符号为止。

fgets()函数的第三个参数指明要读入的文件,如果是从键盘读入数据,那么以stdin作为参数,或者输入文件指针。

当输入行不溢出的时候,fgets()函数将换行符放在结尾,这与fputs()函数的特性相仿:这个函数在打印字符串的时候不会在最后加上换行符。可是如果使用puts()函数一起使用,那么可能就会发现出现了两个换行。

fputs()函数接受两个参数:第一个指出要写入的字符串的位置,第二个指出目标写入的位置,如果输出到屏幕上,那么输入stdout作为参数。

fgets()返回指向char的指针,如果一切顺利,函数返回的地址和传入的第一个参数相同,如果传到文件的结尾,将返回一个特殊的指针:空指针(null pointer),这个指针不会指向有效的数据,所以可以用来标识特殊情况。在代码钟可以用数字0来代替,但是C利用宏NULL来代替。下面是一个很有意思的例子:

1
2
3
4
5
6
7
8
char words[10];
while(fgets(words,10,stdin) != NULL && words[0]!='\n')
{
    fputs(words,stdout);
}

//Input :By the way,it returns a NULL pointer.
//Output:By the way,it returns a NULL pointer.

这个程序的实际操作过程是:首先fgets()函数读入9个字符,在后边加入\\0之后交给fputs()函数输出,但是此时不输出换行符,接着进入下一轮迭代,fgets()函数继续读入字符、交给fputs()函数输出……

  • gets_s()函数

  • s_gets()函数

我们可以利用fgets()函数自行创建一个读取整行输入,并且利用空字符取代换行符、或者读取一部分字符,丢弃溢出的字符(其余部分的字符)的函数:

char *s_gets(char *st, int n){
    char *ret_val;
    int i = 0;
    ret_val = fgets(st, n, stdin);
    if(ret_val){
        while(st[i] != '\n' && st[i] != '\0')
            i++;
        if(st[i] == '\n')
            st[i] = '\0';
        else
            while(getchar()!= '\n')
                continue;
    }
    return ret_val;
}

利用字符串函数,我们可以对函数进行修改,让它更加简洁。

char *s_gets(char *st, int n)
{
    char *ret_val;
    char *find;

    ret_val = fgets(st, n, stdin);
    if(ret_val)
    {
        find = strchr(st, '\n');
        if(find)
            *find = '\0';
        else
            while(getchar() != '\n')
                continue;
    }
    return ret_val;
}

如果fgets()函数返回NULL,则证明读到文件结尾或者读取错误,s_gets()函数跳过了这个过程。

我们丢弃多余的字符的原因是:这些多余的字符都存储于缓冲区之中,如果我们下一个要读取的数据是double类型的,那么就可能造成程序崩溃(因为输入了char类型甚至char*类型的数据),丢弃剩余行的数据可以令读取语句和键盘输入同步。

这个函数并不完美,因为它在遇到不合适的输入的时候毫无反应,并且丢弃多余的字符的时候,不会告诉程序也不会告诉用户。但是至少会比gets()函数安全的多;-)。

  • scanf()函数

scanf()函数和%s转换说明可以读取字符串,但是scanf()函数在读取到空白字符(包括空格、换行符和空字符)的时候会终止对字符串的读取。scanf()函数还有另外一种确定输入结束的方法,也就是指定字符宽度,比如%5d,那么scanf()将在读取完五个字符或者读取到第一个空白字符后停止。

5.2.4 字符串输出

  1. 在输出字符串的时候,我们必须确定字符串末尾有指示终止的空字符,下面就是一个错误的例子:char words[]={'H','e','y','!'};

由于这个字符数组(不是字符串!)结尾并未有空字符,所以words不是字符串,如果我们使用这样的代码:put(words)puts()函数由于未识别到空字符,就会一直向下读取、输出后续内存中的内容,这或许是 garbage value ,直到读到内存中的空字符(内存中还是有不少空字符的)。

  1. puts()函数很容易使用,只需要传入需要输出的字符串的地址就可以了,它在输出的时候会在后边加上一个换行符。但是puts()函数的返回值众说纷纭:某些编译器返回的是输出的字符的个数,某些编译器输出的是输出的最后一个字符,有的干脆就返回一个非零的整数。

  2. fputs()函数需要接受两个参数,一个是字符串的地址,另一个是写入的地址:这一般是文件指针,如果需要输出到屏幕上,传入stdout则可。这个函数的特点在于不会输出换行符。

  3. printf()函数需要转换说明,它的形式更复杂些,需要输入更多的代码,计算机执行的时间会更长,但是优点在于可以更容易地输出更复杂、更多的字符串。

5.2.5 自定义输入输出函数

5.3 字符串函数

这里讲的字符串函数是指定义在头文件string.h内的函数。

  1. strlen()函数

strlen()函数的实现其实很简单,我们写一个while循环就好了(),strlen()函数接受字符串地址,返回一个unsigned int的值来表示字符串的长度。

重要的是我们可以利用strlen()函数来得到字符串的一些性质参数,进而更容易实现对字符串的操作,比如我们可以利用下面自行设计的函数来实现字符串的截断:

char *vit(char str[],unsigned int point)
{
     unsigned int length = strlen(str);
     if(point > length - 1)
     {
         return str;
     }
     else
     {
         str[point]='\\0';
         return str;
     }   
 }
 //或者:if(strlen(str)>point)
 //      {
 //      str[point] = '\\0';
 //  }
  1. strcat()函数

strcat()函数接受两个字符串作为参数,用于将两个字符串拼接在一起,更确切地说是将第二个字符串的拷贝附加在第一个字符串的末尾,并且将拼接后的字符串作为第一个字符串,第二个字符串不变。strcat()函数返回第一个参数。

strcat()函数和gets()函数一样,如果使用不当,也会导致缓冲区溢出。但是gets()函数被废弃的原因在于无法控制用户向程序里边输入什么,但是程序员是可以控制程序干什么的。因此,在经历输入的检查之后,我们认为至少程序是比较安全的,而使用strcat()函数不当导致缓冲区溢出的情况,被认为是程序员粗心导致的,而C语言相信程序员,程序员也有责任确保strcat()函数的使用安全。

  1. strncat()函数

为了避免strcat()函数的不安全的可能,我们类似fputs()函数那样,添加第二个参数,确定最大添加字符数,这就是strncat()函数的逻辑。

strncat()函数接受三个参数,两个字符串指针和最大添加字符量,在加到最大字符量或者遇到空字符的时候停止。

配合strlen()函数,strncat()函数可以很好用。

  1. strcmp()函数和strncmp()函数

首先,我们比较两个字符串的时候,比较的是字符串的内容,而不是字符串的地址,所以我们不能做判断指针是否相等的操作,而利用循环挨个判断还蛮复杂,这就是strcmp()函数诞生的逻辑。

strcmp()函数接受两个字符串指针参数,如果字符串内容完全相等(包括大小写),strcmp()函数就会返回0,否则返回非零值。

在字符串内容不一样的时候,如果第一个字符串的字符在ASCII码在第二个字符串的之前,strcmp()返回负数,反之返回正数;在某些编译器中,会作更加复杂的操作,也就是返回两字符的ASCII码的差。

strcmp()函数会一直比较字符是否相同,直到出现不同或者字符串结束,这样的比较方式显得就非常笨重,而strncmp()函数提供了一种更为灵活的选择:strncmp()函数接受的第三个整数参数指定了比较到第几个字符(这里从1开始计数 ;-) )比如strncmp(str1,"strings",7)就指定只查找strings这七个字符。

  1. strcpy()函数和strncpy()函数

strcpy()函数

  1. sprint()函数

  2. memcpy()函数

  3. Others.

  • strchr()函数

  • strrchr()函数

  • strstr()函数

  • atoi()函数

  • Character Classification

    isalpha()函数

    isalpha()函数属于一类的函数还有

    tolower()toupper()函数

    cppreference 上对这两个函数归类为 Character Manipulation 解释是:converts a character to lowercase/uppercase.

Chapter 6:存储类别和内存管理

6.1 存储类别

6.1.1 总览

6.1.2 关键词

  • 其实这种关键词叫做存储类别说明符。
  • static关键词让变量具有内部链接,同时具有静态存储期。
  • extern关键词让变量具有外部链接,同时具有静态存储期。

6.1.3 作用域

作用域描述程序中可以访问标识符的区域,包括:块作用域,函数作用域,函数原型作用域和文件作用域。

  • 是用一对花括号括起来的代码区域,包含for循环、while循环、do while循环和if语句所控制的代码,就算这些代码没有被花括号括起来,这也算是一个块。定义在块中的变量具有块作用域(block scope),它的可见范围只是在块内,或者说从定义处到包含该定义的块的末尾。此外,函数的形式参数虽然在花括号表示的块之前,但还是具有块作用域。只有在块内的语句才能访问具有块定义域的变量。

  • 函数作用域仅仅用于goto语句的标签,当这个标签首次出现在函数的内层时,作用域也延伸到整个函数。函数作用域有效防止了标签混乱的情况发生,当然更好的处理方式或许是干脆不用goto语句()

  • 函数原型作用域的作用范围时从形式参数定义处到函数原型声明结束。这表明编译器更多的关心形式参数的类型而不是形参名,而只有在变长数组中,形参名才更有用。

  • 如果在函数的外边定义了一个变量,比如以下程序:

1
2
3
4
5
6
#include<stdio.h>
int glb_val =1;
int main(void)
{
    printf("%d",glb_val);
}

这里的变量glb_val就具有文件作用域,更确切地说,具有外部链接的文件作用域,我们也叫它为全局变量。

Tip:这里的glb_val它的作用域是从定义处到文件结束。

6.1.4 翻译单元

某些我们认为的多个文件可能在编译器里边以单个文件的形式出现,比如C预处理器就将头文件里边的内容替换#include指令。所以,编译器将源代码文件和所有的头文件都看作是一个包含着信息的单独文件,这个文件被称为是翻译单元(translation unit)。

如果程序由多个源代码文件组成,那么这个程序也由多个翻译单元组成,每个翻译单元对应着一个源代码文件和它的头文件。

目前我们的程序还不进行多文件处理。

6.1.5 链接

C 文件有着三种链接属性:外部链接、内部链接和无连接。具有块作用域、函数作用域和函数原型作用域的变量都是无连接变量。具有文件作用域的变量可以是外部链接也可以是内部链接。具有内部链接的变量只能在一个翻译单元使用,而具有外部链接的变量能在多文件程序中使用。

使用extern关键词,或者直接在函数外边定义的变量都是具有外部链接的变量,而使用static关键词的变量是具有内部链接的变量。

6.1.6 存储期

存储期(storage duration)描述了通过这些标识符访问的对象的生存期,某些变量存储期一过,它所占的内存就会被释放,相应的,存储的内容也会丢失。C对象有着四种存储期:静态存储期、自动存储期、线程存储期和动态分配存储期。

  • 如果对象具有静态存储期,那么对象在程序的执行期间就会一直存在,并且只在主调函数前初始化一次,内存不会被释放。关键词externstatic表明了对象的链接属性与存储期(静态存储期)。The static/extern specifier specifies both static storage duration (unless combined with _Thread_local) and internal/external linkage.
  • 具有块作用域的变量一般具有自动存储期,当程序进入这些变量的块的时候时,为这+些变量分配内存,当退出这个块的时候,就释放刚才分配的内存。值得注意的时,变长数组的存储期是声明处到块的末尾。
  • 线程存储期用于并发程序设计
  • 动态分配存储期

6.1.7 自动变量

声明在函数头、块内的变量属于自动存储类别的变量,具有自动存储期,块作用域且无连接。我们可以在C中使用关键词auto来表明这个变量的存储类型是自动变量。

6.1.8 寄存器变量

我们使用关键词register来表示该变量的存储类型为寄存器变量。寄存器变量存储在CPU的寄存器之中,寄存器是计算机最快的可用内存,因此访问并且处理这些变量的速度会更快,但是无法获取寄存器变量的地址(因为它没有内存位置)。寄存器变量在绝大多数方面都和自动变量一样,也就是具有块作用域、无链接和自动存储期。

声明变量为register类型更像是一种请求而不是命令,因为编译器必须根据寄存器或者最快可用内存数量来衡量请求;并且由于寄存器的大小有限(通常是一个字,亦即4或8字节),可以声明为寄存器变量的数据类型有限,比如寄存器可能就没有足够大的空间来存储double类型的值。计算机很可能会忽略我们的请求,变量则被声明成一般的自动变量(也就是存储在内存之中),即使这样,仍然不能对该变量使用取地址运算符。

6.1.9 块作用域的静态变量

我们可以创建具有块作用域、无连接的静态变量,只需要在块中(这样就提供块作用域和无连接了)用存储类别说明符static(提供静态存储期)说明这个变量就可以了。

编译器在程序的生命周期内保证静态变量的存在,静态变量只会在程序中被初始化一次,不会在离开和进入作用域时被销毁或者重置。这是因为静态变量和外部变量在程序被载入内存的时候已经执行完毕,所以在逐个步骤调试的时候会发现含有 static声明的变量不太像时程序中的变量 ;-)

6.1.10 外部链接和内部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期,该类别有时被称为外部存储类别,属于该类别的变量称为外部变量。如果未初始化外部变量,则其被默认初始化为0只能用常量表达式初始化文件作用域变量(除了变长数组以外,sizeof()表达式可以看作常量表达式)。

全局变量在main()函数执行之前完成初始化。

我们在文件之间共享全局变量的时候需要特别小心,可以使用以下两个策略:其一,遵循外部变量的常用规则,亦即在一个文件之中使用定义式声明,在另一个文件之中使用引用式说明(使用extern关键字);其二,将需要共享的全局变量放在一个头文件之中,在其他文件中包含这个头文件就可以了,然而,这种处理方式需要我们在头文件中使用static关键词,如果我们不使用static关键词或者使用extern关键词,那么我们就在每一个文件之中都包含了一个定义式声明,C标准是不允许这样子的。然而头文件实际上是给每一个文件提供了一个单独的数据副本,数据是重复的,浪费了很多的内存。

1
2
3
4
5
6
int exint = 1;          //declaration 1
extern int falseint;    //declaration 2
int main(void){
    /*内部不表*/
    extern int exint;   //declaration 3
}

考虑上面的例子:对于外部变量来说,第一次声明declaration 1被称为定义式声明(defining definition),为变量预留了存储空间;第二次声明declaration 3被称为引用式声明(referencing definition),关键词extern表明此次声明不是定义,指示编译器到别处查询定义,这表明declaration 2是不正确的,这时编译器假定falseint定义在程序别处,不会引起分配空间。因此我们不要用extern关键字创建外部定义只使用它引用外部定义

使用关键字static可以声明内部链接的静态变量,只需要在函数外使用static声明就可以,并且在函数内使用时使用extern进行引用式声明即可,但是extern并不改变链接属性

6.1.11 存储类别和函数

函数也有存储类别,可以是外部函数(默认)、静态函数或者内联函数。

  • 使用extern关键词定义的函数是外部函数,是为了表明当前文件中使用的函数被定义在别处,除非使用static关键词,一般函数声明都默认为extern
  • 使用static关键词定义的函数是静态函数,静态函数只能用于其定义所在的文件。可以在其他文件中定义与之同名的函数,这样子就避免了名称冲突的问题。
  • 内联函数:inline

6.2 动态分配内存

我们在前面所探讨的存储类别都有一个共同之处,在确定好存储类别之后,就只能根据确定好的内存存储规则,自动指定存储期和作用域。但是我们也可以利用库函数灵活分配和管理内存,只不过必须好好利用指针。

我们下面讨论malloc()free()calloc()realloc()函数。

6.2.1 void* malloc(size_t size)函数

malloc()函数接受一个参数:所需要的内存字节数,之后它会找到合适的内存块,匿名分配sizebyte大小的内存,返回动态分配内存块的首字节地址。如果无法分配内存,malloc()函数就会返回一个空指针。最早,由于char类型只占用一个字节,所以malloc()函数返回一个char *类型的指针,后来malloc()返回void *类型的通用指针,指向什么都可以,完全不需要考虑类型匹配的问题,但是为了增加代码的可读性,应该坚持强制类型转换。

我们可以利用malloc()函数提供第三种声明数组的方式:将调用malloc()函数的返回值赋给指针,利用指针访问数组的元素,这样创建的其实是一个动态数组。比如:

double *ptd;
ptd = (double *) malloc(30*sizeof(double));

我们完全可以使用正常声明数组一样的方式访问这个数组ptd,比如ptd[18]

malloc()函数也可以声明多维数组,但是语法会复杂一些:

1
2
3
4
5
6
7
8
int numrow = 6,numcolumn = 5;
int **array2 = (int **)malloc(sizeof(int*)*numrow);
for(int i = 0; i<m; i++)
{
    array2[i] = (int *)malloc(sizeof(int)*numcolumn);
}
//或者
int (* array)[numcolumn] = (int (* array)[numcolumn])malloc(sizeof(int)*numcolumn*numrow);

先看第一种定义方式:在第二行创建了一个二级指针,也就是存储着指针的数组array2,在接下来的循环中,逐个为二维数组的每一行分配空间,同时将数组指针存储在array2[i]中。在读取元素array2[1][2]的时候,我们先读取出array2[1],发现是个指针(其实是数组),然后读取这个数组的第三个元素(编号是2),这样就读出来了元素array2[1][2]

再看第二种定义方式:简而言之,等号左侧定义了一个指针变量array,指向的是int[numcolumn]类型的指针,说白了array也是一个二级指针。如果还要整花活,我们发现*(*(array+1)+2)array[1][2]其实是一样的。换句话说,array指向一个内含6个整型的数组,因此array[i]表示一个由numcolumn个整数构成的元素,array[i][j]表明一个整数。

逻辑上看,二维数组是指针的数组(亦即二级数组);但是从物理上来看,二维数组是一块连续的内存,对于二维数组array3[4][5]:我们完全可以按照5进制来理解这块内存的排布,五进制数 ij 表示的数所对应的内存上边的内容就是array[i][j]存储的内容。

6.2.2 void* calloc(size_t num,size_t size)函数

calloc()函数分配numsize大小的连续内存空间,并且将每一个字节都初始化为0,所以calloc()调用的结果是分配了num*size个字节长度的内存空间。calloc()函数的返回值和malloc()函数的一样。

6.2.3 void* realloc(void* ptr,size_t new_size)函数

realloc()函数重新分配指针ptr指向位置内存块的大小。函数返回新分配内存块的起始位置的地址,并且原指针ptr在调用后不再可用。

realloc()函数被调用时,只会做下面两种行为之中的一种:

  • 其一,将ptr指向的内存区域扩大或者缩小,并且尽可能保留剩余原有区域的内容(The contents of the area remain unchanged up to the lesser of the new and old sizes.),如果内存区域扩大,新的内存内容为未定义的(the contens of the new part of the array are undefined.)。
  • 其二,重新分配一块新的内存,将原内存区域的内容拷贝过来,并释放原内存(copying memory area with size equal the lesser of the new and the old sizes, and freeing the old block.)。

6.2.4 void free(void *ptr)函数

free()函数接受先前被malloc(),calloc(),realloc()动态分配过的内存地址,之后将这些内存释放(deallocate),如果free()接受一个空指针,那么它什么都不会做。free()函数不返回任何值。如果free()函数接受的参数不是先前被malloc(),calloc(),realloc()分配过的内存地址,它的行为并未被定义。(The behavior is undefined if the value of ptr does not equal a value returned earlier by malloc(),calloc(),realloc())我们也不能释放同一内存两次(The behavior is undefined if the memory area referred to by ptr has already been deallocated, that is, free(), free_sized(), free_aligned_sized() (since C23), or realloc() has already been called with ptr as the argument and no calls tomalloc(), calloc(), realloc() or aligned_alloc() (since C11) resulted in a pointer equal to ptr afterwards.)。

最重要的是:动态分配的内存必须被释放,否则会发生内存泄漏(memory leak)

6.3 ANSI C 类型限定符

值得注意的是,C99标准为限定符增加了一个新的属性:幂等性。也就是说可以在同一个声明之中使用多个相同的限定符,多余的限定符将被忽略。

6.3.1 const限定符

const关键词声明的对象将成为只读变量,其值不能通过赋值、递增或递减等方式修改,但是至少初始化变量是没问题的,这样我们就只可以使用但不能修改对象的值了。

如果对指针使用const限定符,如果const限定符在*的前面,也就是const int *num或者int const *num,其实限定了指针指向的值为constnum指向了一个int类型的const值。如果const限定符在*的后面,也就是int * const num,则我们创建的指针本身的值不能改变,但是它指向的值可以改变。

更加常见的用法是声明为函数形参的指针。比如void display(const int array[], int num),另外一个更熟悉的例子是字符串函数void strcat(char * restrict string1,const char * restrict string2)。 这使得传进去的数组的值没有被修改,这其实表明了const限定符实际提供了一种保护数据的方法。

我们同样可以对全局变量使用const限定符保护数据,因为extern限定符使得程序的任何一个部分都能使用并且改变这个变量,所以会平白无故产生许多危险,而const限定符让变量变成只读变量,这样就可以另程序更加安全。

6.3.2 volatile限定符

6.3.3 restrict限定符

Chapter 7:文件处理

7.1 文件和文件类型

7.1.1 文件

文件其实是硬盘上的一段已经被命名的存储区域,C将文件看成一系列连续的字节,每一段字节都可以被单独读取。

7.1.2 文件模式

C提供两种文件模式:文本模式和二进制模式。

  • 所有文件的内容都以二进制形式存储,但是如果文件最初使用二进制编码的字符表示文本,那么这个文件就是文本文件,其中包含文本内容。如果文件中的二进制值表示机器语言代码或者数值数据或者图片以及音乐编码,那么这个文件就是二进制文件,其中包含二进制内容。
  • C提供了两种访问文件的途径:文本模式和二进制模式。在二进制模式之中,程序可以访问文件的每一个字节,而在文本模式之中,程序看见的内容和文件实际的内容不同,换行符会进行不同样式的映射转换。

7.2 基本的文件处理

7.2.1 标准文件和标准I/O

C程序会自动打开三个文件:标准输入、标准输出和标准错误输出。通常时候下,标准输入是普通的输入设备,一般是键盘;标准输出和标准错误输出都是系统的普通输出设备,一般是显示屏。函数getchar()、函数printf()和函数puts()都使用的是标准输出。标准错误输出提供了一个逻辑上不同的地方来显示错误输出,如果我们将输出发送给文件,那么发送到标准错误输出的内容仍然会被发送到屏幕上。

7.2.2 基本文件处理

  • [[noreturn]] void exit(int exit_code)函数

exit()函数关闭所有打开的文件并且结束程序,正如函数声明处所说

  • File *fopen(const char *restrict filename, const char *restrict mode)函数

fopen()函数打开一个文件,其文件名由传入函数的第一个参数标识,返回文件指针。其需要的第二个参数是一个字符串,指定了待打开文件的模式。

我们常见的打开文件模式有下面这些:

  • "r" 以只读模式打开文件;
  • "w" 以写模式打开文件,并且将现有文件的长度截为 0,如果文件不存在,则创建一个新文件;
  • "a" 以写模式打开文件,在现有文件结尾添加内容,若文件不存在,则创建一个新文件;
  • "r+" 以更新模式打开文件,亦即可以读写文件。

如果打开文件失败,且不创建新文件,返回一个空指针

值得注意的是,文件指针并不指向任何实际文件,只是指向一个包含文件信息的数据对象(换句话说是一个结构)其中包含了操作文件所用函数所需要的缓冲区信息。

  • int fclose(FILE *stream)函数

fclose()函数关闭由stream给出的文件流,无论关闭是否成功,stream均与这个文件无关。The behavior is undefined if the value of the pointer stream is used after fclose returns.

如果关闭成功,fclose()函数返回0,反之返回EOF

  • int fprintf(FILE *restrict stream, const char *restrict format, ...)函数

fprintf()函数和printf()函数基本相同,只不过输出流从默认的stdout变成了需要自行给出的stream,亦即函数接受的第一个参数表示需要输出的位置。

  • int fscanf(FILE *restrict stream,const char *restrict format, ...)函数

这个函数和scanf()函数大差不差,只不过接受的第一个参数需要是待读取文件的文件指针。

  • char *strerror(int errnum)函数

返回一个指针,指向错误代码errnum代表的文字描述。errnum一般需要从变量errno中取得。

  • long ftell(FILE *stream)函数

ftell()函数返回一个long类型的值,为stream的位置标识符的当前值,亦即。如果出现错误,ftell()函数将会返回-1,全局变量errno被设置为一个正值,我们可以使用errno变量来查看错误代码。比如:

1
2
3
4
5
6
if(fseek(fp, 0L, SEEK_END) == -1)
{
    fprintf(stderr,"ERROR:%s\n",strerror(errno));
    fclose(fp);
    return 1;
}
  • int fseek(FILE *stream, long offset, int origin)函数

fseek()函数将文件看做是数组,将位置标识符stream移动到目标位置。函数的第三个参数是模式,这个参数确定起始点。stdio.h 头文件内有三个表示模式的文件常量:SEEK_SET 表示文件开始处;SEEK_CUR 表示当前位置;SEEK_END 表示文件末尾。第二个参数是相对于origin的偏移量,以字节为单位,可以为正值(前移)、负值(后移)或者0(保持不动)。

如果一切正常,fseek()返回0,若出现错误(比如试图移动的距离超出文件范围了),返回值为-1

ftell()函数在文本模式和在二进制模式的工作方式不同,ANSI C规定,ftell()函数的返回值可以当做fseek()函数的第二个参数。对于MS-DOS,ftell()返回的值将\r\n当做一个字节计数。

  • 函数

  • 函数

  • 函数

  • size_t fread(void *restrict buffer, size_t size, size_t count, FILE * resrict stream)函数

fread()函数接受的参数和fwrite()相同。在fread()函数之中,buffer是待读取文件数据在内存之中的地址,stream指定要读取的文件,该函数可以用于读取文件之中的数据,size代表着待读取数据每个元素的大小,count代表待读取项的项数。函数返回成功读取项的项数,一般是count,如果出现错误或者读到EOF,返回的值就会比count小。

值得一提的是:The file position indicator for the stream is advanced by the number of characters read.

  • size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)函数

fwrite()函数将缓冲数组buffer里面的count个元素写入到流stream之中。函数将需要写入的数据重新编译为unsigned char类型的数组,通过重复调用fptc()函数将其写入stream之中。和fread()函数相同,The file position indicator for the stream is advanced by the number of characters read. 在实际使用fread()函数和fwrite()函数读写文件之中的数据时,文件位置指示符不断前进,这使得我们不会重复读取数据,亦即可以实现这样的操作,一个循环就能实现文件从头读到尾的操作:

1
2
3
4
5
 while(fread(&buffer, sizeof(int16_t), 1, input))
{
    buffer *= factor;
    fwrite(&buffer, sizeof(int16_t), 1, output);
}

7.3 流简介

在C中,我们处理的是流是一系列连续的字节,不同属性和不同种类的输入由属性更加统一的流来表示。流告诉我们,我们可以利用和处理文件相同的方式来处理键盘输入。打开文件的过程就是把流与文件相关联,读写都通过流来完成。

在标准头文件stdio.h之中,定义了三个文本流stdin stdout stderr

Chapter 8:结构和其他数据形式

8.1 结构

8.1.1 结构的定义与基本属性

结构的声明描述了一个结构的组织布局:

1
2
3
4
struct book{
    int index;
    int val;
};

结构体struct book描述了由两个整数类型组成的一个结构体,后续程序中可以利用模板struct book来声明具有相同数据组织结构的结构变量。

一般使用的结构初始化方式有三种,除了对于每一个结构成员进行值初始化之外,可以直接进行列表初始化和利用初始化器初始化:

struct book bk1 = {231, 1219};
struct book bk2 = {.val = 2023, .index = 1, 23};

指向结构的指针具有指针的优点。指向结构的指针比结构本身更加容易操控、在函数中执行更有效率,并且有可能数据表示数据的结构中包含指向其他结构的指针。结构和数组不同,结构名代表的是这个数据集合,而不是结构变量的地址。

结构允许嵌套,也允许使用匿名结构,还允许定义结构数组,但是结构中的的匿名结构就很恶心。

在两个结构类型相同的情况下,我们允许将一个结构赋值给另外一个结构,这是将每个成员的值都赋给另外一个结构的相应成员。

结构不仅可以作为参数传递,也可以作为返回值返回。与传递结构参数相比,传递结构指针执行起来很快,并且在不同版本的C上都可以执行;缺点是不能保护数据(const限定符就可以解决了)。将结构作为参数传递的优点是,函数处理的是原始数据的副本,可以保护数据,但是需要拷贝副本,浪费了时间和内存。

8.1.2 结构中的字符数组和字符指针

我们可以在结构之中使用以下两种方式存储字符串:

#define LEN 20
struct names{
    char first[LEN];
    char last[LEN];
}

struct names1{
    char *first1;
    char *last1;
}

如果仅仅是声明数组并且随即初始化,那么两种声明方式是没有区别的。但是如果使用类似于scanf("%s",&names1.first1);的方式,第二种声明方式就会出现危险。因为程序并未给first1分配存储空间,它对应的是存储在静态存储区的字符串字面量,换言之,结构体names1里存储的只是两个字符串的指针,如果仍要实行该操作,因为scanf()函数将字符串放在names1.first1对应的地址上,但是由于这是没有经过初始化的变量,地址可以是任何值,因此程序会将字符串放在任何地方。简而言之,结构变量中的指针应该只是用来程序中管理那些已经分配或者分配在别的地方的字符串。

如果使用malloc()函数来分配内存并且用指针来存储地址,这样处理字符串就会比较合理,当然需要记得使用free()释放相关内存。

8.1.3 复合字面量和结构

复合字面量可以用于数组或者结构,如果活只需要一个临时结构值,符合字面量很好用,比如我们可以使用复合字面量创建一个结构赋给另外一个结构或者作为函数的参数。语法是将类型名放在圆括号之中,后面紧跟花括号括起来的初始化列表。比如(某些代码进行了适当的精简):

struct book{
    char title[10];
    char auther[10];
    double value;
}
struct book POMA = (struct book){
    "thistitle",
    "thisman",
    11.11
};
int cal(struct book tmp);
cal((struct book){"thattitle","thatman",22.2});

8.1.4 伸缩型数组成员

利用伸缩型数组成员这一特性声明的结构,起最后一个数组成员具有一些特性:其一,该数组不会立刻存在;其二,使用这个伸缩型数组成员可以编写合适的代码,就好像这个数组确实存在并且具有所需数目的元素一样。但是对这个数组有这以下要求:首先,这个数组成员必须是结构的最后一个成员;其次,结构中至少拥有一个成员;最后,伸缩数组的声明类似于普通数组,只不过其中括号是空的。另外,声明一个具有伸缩型数组成员的结构时,我们不能使用这个数组干任何事,必须先给它分配内存空间后才能以指针形式使用它。比如:

1
2
3
4
5
6
7
8
struct flex{
    size_t count;
    double average;
    double array[];
}
struct flex *ptr;
ptr = malloc(sizeof(struct flex) + n*sizeof(double));
ptr->count = n;

使用伸缩型数组成员的结构具有一下要求:第一,我们不能用结构进行赋值或者拷贝,比如*ptr1 = *ptr2,不然这样只能拷贝非伸缩型数组成员外的所有成员,如果非要拷贝,应该使用mencpy()函数;第二,不要按值方式将这种结构传递给函数,应该传递指针;第三,不应该将使用伸缩型数组成员的结构作为数组成员或者另外一个结构的成员。

8.2 联合

联合(union)是一种数据类型,可以在同一个内存空间之中存储不同的数据类型(但是不是同时)。其典型用法是,设计一种表以存储既无规律,实现也不知道顺序的混合类型。使用联合类型的数组,每个联合都大小相等,每个联合可以存储各种数据类型。

1
2
3
4
5
union hold{
    int digit;
    double bigfl;
    char letter;
};

以上声明的联合可以存储一个int类型、一个double类型或者一个char类型的值。联合只能存储一个值,其占用的内存空间是占用内存最大类型所占用的内存。使用联合的方法如下:

1
2
3
union hold fix;
fix.digit = 1;          //把1存储在fix,占用两个字节
fix.bigfl = 1.1;        //把先前存储的内容抹去,把1.1存储在fix,占用八个字节

8.3 枚举

可以使用枚举类型(enumerated type)声明符号名称表示整型常量。使用关键字enum可以声明枚举类型并且指定其可以具有的值(事实上,enum常量就是int类型,所有可以使用int类型的地方都可以使用枚举类型)。enum类型的用法如下:

1
2
3
enum spectrum {red, orange, yellow, green = 100, blue, violet};
enum spectrum color = red;
printf("%d, %d, %d, %d", red, orange, blue, violet);    //输出为0, 1, 101, 102

在使用完枚举声明后,red等就成了具有名称的常量。第二个语句声明了枚举变量color,并且拿red的值初始化color。这种不连续的枚举直接按照整数进行处理,无法按照想象中直接遍历枚举内的元素。

8.4 typedef

利用typedef可以为某一类型自定义名称。我们之前已经利用define进行了自定义名称,但是define只是单纯的文本替换,并且相比于typedef要更为死板很多。比如:

1
2
3
4
#define STRING char*
typedef char* String;
STRING str1, str2;              //其实是char *str1, str2;只创建了一个字符串和一个字符
String strr1, strr2;            //其实是char *strr1, *strr2;创建了两个字符串

这就明白为什么define只不过是单纯的文本替换了。同时,利用typedef为结构变量命名的时候,可以省略掉这个结构的标签,比如:

1
2
3
typedef struct{
    int data; int index;
}pair;

其实这是为一个匿名结构命名。typedef更好的一点是可以为更加复杂的类型命名:比如typedef char (* FRPTC()) [5];FRPTC声明为一个函数类型,这个函数返回一个指针,指针指向内含五个char元素的数组。

8.5 函数和指针

Chapter 9:位操作

9.1 进制、位和字节

9.1.1 计数

计算机基于二进制,通过关闭和打开状态的组合来表示信息。C语言利用字节表示存储系统字符集所需的大小,通常一字节(byte)包含八(bit),计算机界用八位组(octet)特指八位字节。可以从左到右给这八位编码为7 ~0,编号为7的位被称为高阶位,编号为0的被称为低阶位。

如何利用二进制表示有符号整数取决于硬件,最通用的做法是利用补码。二进制补码利用一字节的第一位(高阶位)表示数值的正负,如果高阶位是0,则此时表示非负数,其值与正常情况相同;如果高阶位为1,则此时表示负数,但是这时负值的量是九位组合100000000(256的位组合)减去这个负数的位组合。比如某负数为10011010,其表示-12211111111则表示-1。这样,我们就可以用一字节来表示从-128~127的所有数字。

浮点数分两部分存储:二进制小数和二进制指数。计算机中存储浮点数的时候,要留出若干位存储二进制小数,剩下的位存储指数。二进制小数用有限多个\(1/2\)的幂的和近似表示数字(事实上,二进制小数只能精确表示有限多个\(1/2\)的幂的和)。一般而言,数字的实际值是由二进制小数乘以\(2\)的指定次幂组成。

9.1.2 其他进制数

计算机界常用八进制十六进制计数系统,这些计数系统比十进制更加接近计算机的二进制系统。

  • 八进制(octal)基于八的幂,每个八进制位对应三个二进制位。我们在数字前加上一个0来特别表示一个数是八进制。
  • 十六进制(hexadecimal)基于十六的幂,每个十六进制位对应四个二进制位。我们在数字前加上0x来特别表示一个数是十六进制。十六进制是表示字节的非常好的方式。

9.2 按位运算符

9.2.1 按位逻辑运算符

  • 按位取反(反码):~

  • 按位与:&

  • 按位或:|

  • 按位异或:^

9.2.2 移位运算符

  • 左移:<<
  • 右移:>>

9.2.3 应用

  • 掩码
  • 打开位
  • 关闭位
  • 切换位
  • 检查位的值
  • 快速乘除运算

9.3 位字段

9.4 对齐特性

没看懂,先不写。

Chapter 10 C预处理器和C库

Chapter 11

C常用库

assert

stdio

int sprintf(char *buffer, const char *format,...)函数

向字符串buffer里写入,相当于多了一个转换格式/读写的工具函数。

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

sscanf()函数

fscanf()函数

向输出流stream中写入。

stdlib

x.2.x 随机数

  • void srand(unsigned int seed)

srand()函数为伪随机数生成器rand()播种,正常的用法是:srand((unsigned int) time(NULL))

这段代码利用当前时间为伪随机数生成器rand(0)提供种子,这样子就可以得到了近似于真随机的随机数。

  • int rand(void)

伪随机数生成器rand()生成一个介于0RAND_MAX的随机数。如果没有srand()的播种,rand()函数就会默认生成种子为1的随机数。每次调用rand()函数,我们得到的都是上次生成的随机数的下一个数

值得注意的是,在调用函数rand()之前的时候,伪随机数生成器只应该被播种一次。

Generally speaking, the pseudo-random number generator should only be seeded once, before any calls to rand(), and the start of the program. It should not be repeatedly seeded, or reseeded every time you wish to generate a new batch of pseudo-random numbers.

更重要的是,当rand()接受相同的种子的时候,他会生成相同的随机数数列。

time

x.3.1 变量类型

  • time_t 这是一个适合储存日历时间的长整型(long int)变量,表示着从POSIX time (1970年1月1日00:00)开始的总秒数。

x.3.2 函数

  • time_t time(time_t *seconds)

time()函数将当前日历时间作为一个time_t类型的变量返回,并且将这个变量存储在输入的指针seconds中(前提是这个指针不为空指针)。

由于time_t类型其实是一个long int转换成int(或者unsigned int)的时候还是需要强制转换说明的2