C Primer Plus读书笔记(三)

文章目录

本文是《C Primer Plus》第九章至第十一章读书笔记,持续更新中。

ch09. 函数

  1. 函数原型、函数定义和函数调用
  2. 函数的参数列表和返回值
  3. 形参和实参
  4. 函数类型
  • 函数原型:任何程序在使用函数之前都要声明改函数的类型。在函数调用前需要声明函数原型。函数原型是告诉编译器函数的类型。对于较小的函数,也可以把函数定义放在主调函数之前,这时候的函数定义也就是函数声明

  • 函数定义:提供函数的实际代码

  • 声明带有参数的函数时,可以只给定参数的类型而不需要提供参数的名字,比如:void show_n_char(char, int);

  • 函数参数 - 主调函数通过参数将值传递给被调函数

    • 形式参数(formal argument) - 函数定义和声明中,参数列表里定义的参数
    • 实际参数(actual argument) - 具体的值,是主调函数赋值给形式参数的值,可以是常量、变量或者表达式。在被调函数中使用的实际参数的值,是主调函数以拷贝的方式传递给被调函数,因此在被调函数中需值的修改不会影响原始数据
  • 返回值 - 被调函数通过返回值将值传递给主调函数

  • 函数类型,实际上是指函数的返回值类型

  • 递归

    • 每一级递归函数里的变量都是私有的,即都属于对应层级的递归
    • 递归函数可能会快速消耗计算机内存,甚至耗尽。因此递归函数的层级受限于内存空间
    • 此外,递归本质是函数调用,由于递归引发的多次函数调用,对性能会有一定的影响
  • 头文件 - 把函数原型和已定义的字符常量(#define)放在头文件中是一个良好的习惯,这样只需要在使用这些函数或者字符常量的地方通过#include引入头文件即可,避免了重复声明函数原型

  • 指针

    • 是一个值为内存地址的变量,也称为指针变量
    • 取地址操作:ptr = &pooh,将pooh地址赋给指针变量ptr
    • 取值操作/间接运算法操作/解引用操作:val = *ptr。实际上ptr = &pooh; val = *ptr; 等价于val = pooh
    • 声明指针:int * pi; char * pc; float pf
    • void*指针:通用指针,用于指向不同类型的情况。如果形参是void*类型,实参可以是指向不同类型的指针,例如double*,编译器会选择合适的类型
  • 函数参数的值传递和地址传递

    • function(int num) - 值传递,一般用于基于值进行计算
    • function(int* num) - 地址传递,一般用于在被调函数中修改主调函数的值

ch10. 数组和指针

  1. 数组如何创建
  2. 指针如何创建和使用
  3. 数组和指针有何关系
  4. 多维数组
  • 数组:数据类型相同的一系列元素组成。声明数组必须告诉编译器数组的元素个数和元素数据类型

  • 数组初始化:

    • 以逗号分割的值列表(花括号里)来初始化数组,如:int powers[3] = {1,2,3};。注意,这种用花括号形式的赋值只能在初始化的时候,其他时候则不行
    • 经常使用符号常量来指定数组大小,如:int days[MONTHs] = {31,28,31};
    • const数组和变量一样,都是表示只读类型,即初始化后只能读不能写
    • 使用数组前必须初始化,如果不初始化就读取,那么读取到的值不可靠
    • 如果只初始化部分数组元素,剩余的元素的值默认被设置为0
    • 创建数组时可以不指定数组大小,让编译器自动匹配数组大小,如:int days[] = {31,28,31};
  • 指定初始化器

    • 使用指定初始化器,可以给特定下标的元素赋值,其他元素保持默认,如:int staff[] = {1,[6]=4,9,10};,第0号元素为1,第6,7,8号元素分别是4,9和10,剩余元素默认为0
    • 如果多次初始化指定元素,后面的会覆盖前面的
  • 多维数组在计算机内部是按顺序存放数据

  • 二维数组初始化:

    • 可以看成是元素类型为一维数组的一维数组

    • int sg[2][3] = {{5,6},{7,8}} 定义了两行三列的二维数组,但是每行都只初始化了前两个元素,因此每行的第三个元素初始化为0

    • 另一种初始化方式是只用一对大括号,大括号里面的值依次按行付给数组元素。例如:int sg[2][3]={5,6,7,8},这样第一行的元素分别是5,6和7。而第二行是8和0

  • 指针和数组

    • 数组名是该数组首元素的地址
    • 声明数组将分配存储数组的空间,而声明指针只是分配存储一个地址的空间
    • 指针加1表示增加一个存储单元(即:指向下一个存储单元的地址)。这里的存储单元实际上对应指针所指向对象的数据类型对应的字节数
    • 数组加1后的地址是下一个元素的地址
    • 举例来说,定义一个数组ar
      • ar[n] = *(ar+n)
      • ar + n的意义是:先定位到ar的位置,也就是首个元素的地址,然后移动n个存储单元
  • 函数和数组

    • 处理数组的函数实际上是用指针作为参数
    • 在函数原型或者函数定义中,int *arint[] ar等价,也只有在函数原型或者定义中,这二者才能互换。但是只有ar是指针变量的时候,才能使用ar++这种操作,并且不支持++ar
    • 函数原型中指定数组的两种方式:1)通过指针来指定数组首元素的地址 2)使用数组的定义
  • 在函数原型或者定义中指定数组元素个数的两种方式

    1. 在函数原型或者定义中指定数组的大小

      1int sum(int* ar, int n)
      2{
      3	int total = 0;
      4	for (int i = 0; i < n; ++i)
      5	{
      6		total += ar[i];
      7	}
      8	return total;
      9}
      
    2. 在函数原型或者定义中指定数组的开始处和结束处

       1int sump(int* start, int* end)
       2{
       3	int total = 0;
       4	while (start < end)
       5	{
       6		total += *(start++);
       7	}
       8	return total;
       9}
      10int main()
      11{
      12	int numbers[] = { 0, 1, 2, 3, 4 };
      13	printf("sump(): %d\n", sump(numbers, numbers + SIZE));
      14	return 0;
      15}
      

      几点需要注意:

      • end实际上是指向数组最后一个元素的后面
      • C保证在分配数组空间的时候,确保最后一个元素后面的第一个位置的指针仍然有效,但是对存储在该位置上的值未做保证
      • 上面例子中,numbers + SIZE就是指向最后一个元素的后面
  • 指针的几种操作

    • 赋值:ptr = &val,把地址付给指针
    • 解引用:*运算符给出指针指向的地址上存储的值。注意:千万不要解引用未初始化的指针
    • 取值:ptr2 = &ptr1,指针变量也有自己的地址和值
    • 指针与整数n相加:*指针对应的地址+数组元素的数据类型的大小(字节为单位)n, 实际上相当于数组的下标往后移动几个
    • 递增指针:实际相当于加1,数组下标往后移动一个
    • 指针减去一个整数:与整数相加相反,数组下标往前移动
    • 递减指针:数组下标往前移动一个
    • 指针求差:两个指针分别指向同一个数组的不同元素,通过计算求出两个元素之间的距离
    • 比较:两个指针都指向相同的类型才能比较
  • 在函数(参数)中传递数组

    • 数组是以指针的方式传递。如果数组也通过副本的方式传递,那么会占用大量的内容
    • 由于数组时通过指针传递,因此是可以在函数中直接修改参数的值
    • 为了避免在函数中修改参数的值,应该将数组声明为const,例如:int sum(const int ar[], int n);
    • const并不是要求原数组是常量,而是函数处理数组时将其视为常量。如果不小心在函数中修改了数组的值,编译器会报错
  • const的几种用法

    定义指针是否可以修改指向别处指针指向的值是否可以修改
    double rates[3] = {11.1, 22.2, 33.3}; const double * pd = rates;
    double rates[3] = {11.1, 22.2, 33.3}; double * const pd = rates;
    double rates[3] = {11.1, 22.2, 33.3}; const double * const pd = rates;

    **const的修饰对象:**取决于和谁挨得近。如果和数据类型挨得近,那么是修饰对应的数据类型是const类型,即数据类型本身只读,但是指针可以修改指向;如果和*挨得近,则修饰指针,即指针本身的值不可以改变(不可以指向别处)

    此外还需要注意:

    • const数据或者非const数据的地址初始化为指向const的指针是合法的。指向const的指针意义在于无法通过该指针修改相应地址对应的值:

      1double rates1[3] = { 11.1, 22.2, 33.3 };
      2const double rates2[3] = { 44.4, 55.5, 66.6 };
      3const double * pd1 = rates1;
      4const double * pd2 = rates2;
      
    • 只能把非const数据的地址赋给普通指针。由于可以通过普通指针修改指向的地址对应的值,因此如果将const数据的地址赋给普通指针,那么const数据将变的可以修改,这是不合理的,这个将产生不可预期的结果

      1const double rates2[3] = { 44.4, 55.5, 66.6 };
      2double * pd3 = rates2; // 不应该这么做
      
  • 二维数组 - int zippo[4][2] = {{ 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 }}

    • 本质是数组元素为一维数组的一维数组

    • zippo:首元素地址,首元素为一维数组{2, 4},因此又是{2, 4}的首地址,也就是元素2的地址

    • zippo[n]:第n个元素的地址,也就是第n+1行的起始地址

    • 对于二维数组,需要两次解引用才能取到某个具体的值

      表达式解释
      zippo首元素地址,实际上是第一行元素的起始地址。*zippo取到第一行元素,**zippo取到第一行元素的第一个元素
      zippo + 2第3行元素的地址
      *(zippo + 2)第3行元素的首元素地址
      *(zippo + 2)+1第3行元素的第2个元素的地址
      *(*(zippo + 2)+1)第3行元素的第2个元素的值
    • int (*px)[2] - 指向一个二维数组,且这个二维数组有两列,即n * 2。**注意:**要和int *px[2]区分开,因为[]的优先级高于*,因此px先和[]结合这样就声明为元素个数为2的一维数组,然后该一维数组的元素数据类型是int*,也就是:含有两个int指针的一维数组

       1int a1_20[2] = { 1, 2 };
       2int a1_21[2] = { 2, 3 };
       3
       4int a1_3[3] = { 1, 2, 3 };
       5int a1_5[5] = { 1, 2, 3, 4, 5 };
       6
       7int* a2_1[2] = { a1_3, a1_5 };
       8int* a2_2[2] = { a1_20, a1_21 };
       9
      10// 下面这两种都不行。int (*)[2] 表示的指向四个元素的指针,即元素为int[2]的一维数组,也就是n*2的二维数组
      11//	int (* a2_3)[2] = { a1_20, a1_21 };
      12//	int (* a2_4)[2] = {{ 1, 2, }, { 1, 2 }};
      13int a22[][2] = {{ 1, 2 }, { 3, 4 }};
      14int (* a2_5)[2] = a22;
      
    • 函数声明中的参数是二维数组时,下面两种方式的定义都是等价:

      • void somefunction(int (*pt)[4])
      • void somefunction(int pt[][4]) - 对于这种形式,第一个方括号一定留空,这样代表是一个指针,且该指针指向元素为4个元素的数组。第二个方括号里一定有一个值,否则pt这个指针无从知道它所指向的对象大小
  • 定长数组和变长数组

    • 定长数组:数组的长度是固定的,声明的时候需要使用常量或者常量表达式来指定数组的大小
    • 变长数组:创建数组的时候,可以使用变量来指定数组的大小。创建之后,则无法再修改数组大小。比如可以定义一个出来任意行列的二维数组:
      • int sum2d(int rows, int cols, int ar[rows][colds]) - 注意:rowscols必须放在ar的定义前面
      • int sum2d(int,int,int ar[*][*])
  • 复合字面量

    • 字面量 - 5是int类型字面量,'c'是char类型的字面量,81.3是double类型的字面量...
    • (int [])(50,20,90)是一维数组字面量,二维数组类似,这些就是复合字面量
    • 复合字面量可以很方便的给函数传递数组参数而不必事先创建数组,实际上复合字面量就是匿名数组

ch11. 字符串和字符串函数

  1. 字符串的表示形式
  2. C库中的字符和字符串函数的使用

几种定义字符串的方式:

  • 字符串常量 - 用双引号括起来的内容称为字符串常量。编译器会在双引号的字符末尾自动加入\0字符

    • 字符串常量属于静态存储类别,即该字符串只被存储一次
    • 双引号括起来的内容被视为指向该字符串的存储位置的指针。例如:*"space farers"表示的是对"space farers"的存储位置的地址解引用,那么取到的值就是该字符串的首个字符s
    • 如果字符串中带有引号,那么需要在引号前面添加反斜杠
  • 字符串数组和初始化

    • char name[10] = "Jonathan" - 在字符个数少于定义的数组个数时,用'\0'补足。注意,如果用单个字符赋值,例如char name[10] = {'J', 'o', 'n', 'a', 't', 'h','a','n','\0'},最后一个字符一定得是'\0',否则会被认定是字符数组而不是字符串
    • char name[] = "Jonathan" - 这种不给定数组长度,那么编译器自动根据给定的字符串计算长度,当然最后会加上'\0'
  • 数组和指针的区别 - 既然字符串可以使用字符数组来表示,而后数组实际上又是和指针相通,那么指针自然可以表示字符串

    • char * head = "I love you" - 这里的head是指针变量,因此可以修改head使得它指向其他的字符串
    • char heart[] = "I love you" - 这里的heart是常量,因为数组变量的值就是数组首个元素的地址,这个值是固定。因此不可以对heart本身再赋值。比如:heart = head这个是不成立的
    • char * head = "I love you";*(head + 5) = 'T' - 这是不推荐的做法,不同的编译器可能会导致不同的行为。对于"I love you"这个字符串而言是存放在静态存储区,这里通过head修改了对应的静态存储区的值,会导致引用该字符串的所有其他变量都发生变化。因此,建议在把指针初始化为字符串常量的时候使用const修饰符,即:const char * head = "I love you"
    • 对于char heart[] = "I love you"而言,实际上heart并不是指向"I love you"的静态存储区,数组实际上是获得了"I love you"的副本。所以,如果打算修改字符串,应该用数组形式而不是用指针
  • 几种字符串输入方式的比较 - 字符串输入首先是需要预留存储该字符串的空间,然后使用输入函数获取字符串

    • scanf("%s", aStr) - 只能读取一个单词

    • gets(astr) / puts(astr) - 读取整行的输入直到遇到换行符,然后丢弃换行符,接着在末尾添加一个空字符使其成为一个C字符串注意:gets只有一个参数,即字符串的首地址,因此无法判断输入的字符串是否有足够的空间存储。如果超过了存储空间内,会出现不可预知的问题。puts会在字符串末尾添加换行符

    • fgets(astr,len,ioin)/fputs(astr, ioout) - 通常和fputs配合使用。有三个参数1) 字符串变量用于接收字符串 2) 字符串的长度(如果是n,最大那么读入n-1个字符,最后一个字符用\0填充。在存储空间充足的情况下会把换行符也存储下来) 3) 指定要读入的文件,如果是键盘读入,则以stdin作为输入。fputs不会在字符串末尾添加换行符。fgets函数返回指向char的指针。如果读到文件末尾或者文件里空行,则返回一个特殊的指针NULL

      输入函数说明输出函数说明
      scanf("%s", str)以第一个非空白字符作为开始,以下一个空白字符作为结束。如果指定了字段宽度,则根据宽度或者空白字符作为终止printf("%s",str)打印一个字符串,不自动添加换行符
      gets(astr)读取整行,丢弃换行符。如果字符串过长可能导致缓冲区溢出puts(astr)打印一个字符串(最后一个字符是'\0'),自动在末尾添加换行符
      fgets(astr,len,ioin)从流中读取字符串,保留换行符。可能是文件流或者标准输入流。当读到文件结尾时返回NULLfputs(astr, ioout)输入字符串到流,不会自动添加换行符

      注意:gets/putsfgets/fputs应尽量配对使用,否则可能出现换行错乱的情况

      代码解读1:

       1#include "stdio.h"
       2#define STLEN 5
       3int main(void)
       4{
       5	puts("Enter string (empty to exit)");
       6	char words[STLEN];
       7	while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n')
       8	{
       9		fputs(words, stdout); // 如果输入的字符串长度超过STLEN,那么就先处理长度范围内的数据,然后继续处理剩余的
      10	}
      11	puts("fgets Done!");
      12
      13	while (scanf("%s", words) != 0) // 字符串被空格分隔
      14	{
      15		printf("%s\n", words);
      16	}
      17}
      18
      19  ch11 ./a.out
      20Enter string (empty to exit)
      21hello, this is jonathan
      22hello, this is jonathan
      23
      24fgets Done!
      25hello, this is jonathan
      26hello,
      27this
      28is
      29jonathan
      

      代码解读2:

       1#include <stdio.h>
       2#define STLEN 10
       3int main(void)
       4{
       5	char words[STLEN];
       6	int i;
       7	puts("Enter strings (empty line to quit):");
       8	while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n') // 当按下回车后,缓冲区里的内容会发送给fgets函数。fgets会根据STLEN按长度来接收,不要忘记最后一个是'\0',这个'\0'是自动补上去的
       9	{
      10		i = 0;
      11		while (words[i] != '\n' && words[i] != '\0') // 这里就先将下标移动到当前words的最后一个位置。取决于输入的字符串的长度。如果最后一个是回车,那么说明输入的长度小于STLEN
      12			i++;
      13		if (words[i] == '\n') // 长度小于STLEN
      14			words[i] = '\0';
      15		else
      16			while (getchar() != '\n') // 如果输入长度大于STLEN,则丢弃掉超过STLEN之外的字符,这些字符是存在于缓冲区,如果没丢弃掉还会赋值给words
      17				continue;
      18		puts(words);
      19	}
      20	puts("Done");
      21	return 0;
      22}
      23
      24  iwanttodebug ./a.out
      25Enter strings (empty line to quit):
      26hello world
      27hello wor
      28hello
      29hello
      30
      31Done
      
  • 空字符和空指针

    • 空字符:对应的编码是0,是整数类型
    • 空指针:是一个指针类型,指向一个无效的地址,它不会与任何数据的有效地址对应
  • 两个和字符(串)相关的系统库

    • string.h - 大量操作字符串的函数
    • strlib.h - 大量字符串和数值转化的函数
  • 命令行参数

    • 1/* repeat.c */
      2int main(int argc, char* argv[]){...}
      
    • agrc - argument count,命令行参数的个数。通常命令名为第一个参数

    • argv - argument value,字符串数组,存放参数的值

    • 例如:repeat I am fine,argc为4,对应的值存放在argv数组里

    • 例如:repeat "I am fine",这里argc为2,I am fine为一个字符串