欢迎光临散文网 会员登陆 & 注册

ok

2023-08-14 14:51 作者:奇异果Kiwifruit  | 我要投稿

学习笔记 《程序设计实践》 《C程序设计现代方法》《LinuxC一站式学习》

编程不过是把你所想转为代码,用堆栈的角度理解指针

第一章 数组

在C语言中一共有两种聚合类型:数组(array)和结构(structure)。本章介绍一维数组(1.1节)和多维数组(1.2节)的声明与使用。1.3节讨论了C99中的变长数组。本章主要讨论一维数组,因为与多维数组相比,一维数组在C语言中占有更加重要的角色.

1.1声明数组需要指明类型和数量

 //数组惯用法逆序输出
 #include <stdio.h>
 #define N 10
 int main(){
     int n, a[N];
     for (int i = 0; i < n; i ++)
         scanf("%d", &a[i]);
     for (int i = N - 1; i >= n; i --)
         printf("%d", a[i]);
      printf("\n");
     return 0;
 }

下面这个程序用来检查数中是否有出现多于一次的数字。用户输入数后,程序显示信息Repeated digit或No Repeated digit:数28212有一个重复的数字(即2),而9357这样的数则没有。

 /*
  程序采用布尔值的数组跟踪数中出现的数字。名为digit_seen的数组元素的下标索引从0到9,对应于10个可能的数字。最初的时候,每个数组元素的值都为假。(digit_seen的初始化式为{false},这实际上只初始化了数组的第一个元素。编译器会自动把其他元素初始化为0,0跟false是相等的。)
  当给定数n时,程序一次一个地检查n的数字,并且把每次的数字存储在变量digit中,然后用这个数字作为数组digit_seen的下标索引。如果digit_seen[digit]为真,那么表示digit至少在n中出现了两次。另一方面,如果digit_seen[digit]为假,那么表示digit之前未出现过,因此程序会把digit_seen[digit]设置为真并且继续执行。
 */
 #include <stdio.h>
 #include <stdbool.h>
 int main(){
     bool digitSeen[10] = {false};
     int digit;
     long n;
     printf("请输入一个数:");
     scanf("%ld", &n);
     while (n > 0){
         digit = n % 10;
         if (digitSeen[digit]) break;
         digitSeen[digit] = true;
         n / 10;
     }
     if (n > 0)  printf("有重复数字");
  else printf("没有重复数字");
 }

1.2多维数组与常量数组

数组可以有任意维数。可以声明产生一个二维数组(或者按数学上的术语称为矩阵):

就像for循环和一维数组紧密结合一样,嵌套的for循环是处理多维数组的理想选择。例如,思考用作单位矩阵的数组的初始化问题。(数学中,单位矩阵在主对角线上的值为1,而其他地方的值为0,其中主对角线上行、列的索引值是完全相同的。)

 #define N 10
 double a[N][N];
 for (int i = 0; i < N; i ++)
     for (int j = 0; j < N; j ++)
    if (i == j)  a[i][j] = 1.0;
   else         a[i][j] = 0.0;
 

无论一维数组还是多维数组,都可以通过在声明的最开始处加上单词const而成为“常量”:程序不应该对声明为const的数组进行修改,编译器能够检测到直接修改某个元素的意图。把数组声明为const有两个主要的好处。它表明程序不会改变数组,这对于以后阅读程序的人可能是有价值的信息。它还有助于编译器发现错误——const会告诉编译器我们不打算修改数组。const类型限定符(➤18.3节)不限于数组,后面将看到,它可以和任何变量一起使用。但是,const在数组声明中特别有用,因为数组经常含有一些在程序执行过程中不会发生改变的参考信息。

 const char a[] = {
  'a', 'b', 'c', 'd', 'e', 'f', 'g'
 };

下面这个程序说明了二维数组和常量数组的用法。程序负责发一副标准纸牌。每张标准纸牌都有一个花色(梅花、方块、红桃或黑桃)和一个等级(2、3、4、5、6、7、8、9、10、J、Q、K或A)。程序需要用户指明手里应该握有几张牌:

Enter number of cards in hand: 5 Your hand: 7c 2s 5d as 2h

不能明显地看出如何编写这样一个程序。如何从一副牌中随机抽取纸牌呢?如何避免两次抽到同一张牌呢?下面将分别处理这两个问题。

为了随机抽取纸牌,可以采用一些C语言的库函数。time函数(➤26.3节,来自于<time.h>)返回当前的时间,用一个数表示。srand函数(➤26.2节,来自于<stdlib.h>)初始化C语言的随机数生成器。通过把time函数的返回值传递给函数srand可以避免程序在每次运行时发同样的牌。rand函数(➤26.2节,来自于< stdlib.h>)在每次调用时会产生一个看似随机的数。通过采用运算符%,可以缩放rand函数的返回值,使其落在0~3(用于表示牌的花色)的范围内,或者是落在0~12(用于表示纸牌的等级)的范围内。

为了避免两次都拿到同一张牌,需要记录已经选择过的牌。为了这个目的,程序将采用一个名为in_hand的二维数组,数组有4行(每行表示一种花色)和13列(每列表示一种等级)。换句话说,数组中的每个元素对应着52张纸牌中的一张。在程序开始时,所有数组元素都为假。每次随机抽取一张纸牌时,将检查数组in_hand中的对应元素为真还是为假。如果为真,那么就需要抽取其他纸牌;如果为假,则把true存储到与这张纸牌相对应的数组元素中,以提醒我们这张纸牌已经抽取过了。

一旦证实纸牌是“新”的(还没有选取过),就需要把牌的等级和花色数值翻译成字符,然后显示出来。为了把纸牌的等级和花色翻译成字符格式,程序将设置两个字符数组(一个用于纸牌的等级,另一个用于纸牌的花色),然后用等级和花色对数组取下标。这两个字符数组在程序执行期间不会发生改变,所以也可以把它们声明为const。

 #include <stdio.h>
 #include <stdbool.h>
 #include <time.h>
 #include <stdlib.h>
 
 #define NUM_SUIT 4
 #define NUM_RANKS 13
 
 int main(){
     bool inHand[NUM_SUIT][NUM_RANKS] = {false};
     int numCards, rank, suit;
     const char rankCode[] = {'2', '3', '4','5', '6', '7','8', '9',
                              't', 'j', 'q','k', 'a'};
     const char suitCode[] = {'c', 'd', 'h','s'};
     srand((unsigned)time(NULL));
     printf("输入你手中卡牌的数量");
     scanf("%d", &numCards):
     printf("你手中的卡牌");
     while (numCards > 0){
   suit = rand() % NUM_SUIT;
         rand = rand() % NUM_RANKS;
         if (!inHand[suit][rank]){
             inHand[suit][rank] = true;
             numCards --;
             printf("%c%c", rankCode[rank], suitCode[suit]);
         }
     }
     printf("\n");
     return 0;
 }

第二章 函数

函数是C程序的构建块。每个函数本质上是一个自带声明和语句的小程序。可以利用函数把程序划分成小块,这样便于人们理解和修改程序,本章讲解函数的调用声明与定义,以及数组参数的传递和递归

下面这个程序判断素数给定数n后,is_prime函数把n除以从2到n的平方根之间的每一个数;只要有一个余数为0,n就不是素数。

 #include <stdio.h>
 #include <stdbool.h>
 bool isPrime(int n){
     int divisor;
     if (n <= 1) return false;
     for (divisor = 2; divisor*divisor <= n; divisor ++){
         if (n % divisor == 0) return false;
     }
     return true;
 }
 int main(){
     int n;
     scanf("%d", &n);
     if (isPrime(n)) printf("是素数"):
     else            printf("不是素数");
     return 0;
 }

递归

 #include <stdio.h>
 
 const int N = 1e6 + 10;
 int n;
 int q[N];
 
 void quick_sort(int q[], int, int);
 void swap(int*, int*);
     
 int main(){
     scanf("%d", &n);
     for (int i = 0; i < n; i ++) scanf("%d", &q[i]);
     quick_sort(q, 0, n - 1);
     for (int i = 0; i < n; i ++) printf("%d", q[i]);
 }
 void quick_sort(int q[], int l, int r)
 {
     if (l >= r) return;
     int i = l - 1, j = r + 1, x = q[l];
     while (i < j){
         do i ++ ; while (q[i] < x);
         do j -- ; while (q[j] > x);
         if (i < j) swap(q[i], q[j]);
     }
     quick_sort(q, l, j)
     quick_sort(q, j + 1, r);
 }
 void swap(int*a, int*b){
     int temp;
     temp = *a;
     *a = *b;
     *b = temp;
 }

第三章 指针,数组,字符串,指针的高级应用

C语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。

指针作为参数

 #include <stdio.h>
 #define N 10
 void max_min(int a[], int n, int*max, int*min);
 int main(){
     int b[N], big, small;
     for (int i = 0; i < n; i ++)
         scanf("%d", &a[i]);
     max_min(b, N, &big, &small);
     printf("%d", big);
     printf("%d", small);
     return 0;
 }
 
 void max_min(int a[], int n, int*max, int*min){
     *max = *min = a[0];
     for (int i = 0; i < n; i ++){
         if (a[i] > *max) *max = a[i];
         else if (a[i] < *min) *min = a[i];
     }
 }

指针作为返回值

 #include <stdio.h>
 int* max(int* a, int* b){
     if (*a > *b) return a;
     else    return b;
 }
 int main(){
     int a, b;
     max(&a, &b);
 }

C程序员经常在处理数组元素的语句中组合*(间接寻址)运算符和++运算符。思考一个简单的例子:把值存入一个数组元素中,然后前进到下一个元素。利用数组下标可以这样写:

 a[i++] = j;

如果p指向数组元素,那么相应的语句将会是

 *p++ = j;

通常情况下,a + i等同于&a[i](两者都表示指向数组a中元素i的指针),并且*(a+i)等价于a[i](两者都表示元素i本身)。换句话说,可以把数组的取下标操作看成是指针算术运算的一种形式。

 //惯用法
 #include <stdio.h>
 #define N 10
 int main(){
     int a[N], *p, sum = 0;
   printf("输入%d个数", N);
     //a == &a[0]数组的第一个元素
     for (p = a; p < a + N; p ++)
          sum += *p;
          scanf("%d", &N);
     for (p = N - 1; p >= a; p --)
          printf("%d", *p);
       printf("\n");
     return 0;
 }

用指针作为数组名

 #define N 10
 int a[N], sum = 0, *p = a;
 for (int i = 0; i < N; i++)
     sum += p[i];
 //编译器把p[i]看作*(p+i)
 

字符串字面量是作为数组来存储的,那么编译器会把它看作是char *类型的指针,也是指向第一个元素的地址printf函数和scanf函数都接收char *类型的值作为它们的第一个参数。

只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串。这种方法很简单,但使用起来有很大难度。有时很难辨别是否把字符数组作为字符串来使用。如果编写自己的字符串处理函数,请千万注意要正确地处理空字符。而且,要确定字符串长度没有比逐个字符地搜索空字符更快捷的方法了。

假设需要用一个变量来存储最多有80个字符的字符串。由于字符串在末尾处需要有空字符,我们把变量声明为含有81个字符的数组:

字符串的长度取决于空字符的位置,而不是取决于用于存放字符串的字符数组的长度

字符串变量的声明中可以省略它的长度。这种情况下,编译器会自动计算长度:

 #define STR_LEN 11
 char str[STR_LEN + 1];
 
 char str2[] = "hello world";

字符数组与字符指针

 char s1[] = "hello world";
 char* s2 = "hello world";

在声明为数组时,就像任意数组元素一样,可以修改存储在date中的字符。在声明为指针时,date指向字符串字面量,在13.1节我们已经看到字符串字面量是不可以修改的。·在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序执行期间指向其他字符串。如果希望可以修改字符串,那么就要建立字符数组来存储字符串,声明指针变量就不够的

在使用p作为字符串之前,必须把p指向字符数组。一种可能是把p指向已经存在的字符串变量:现在p指向了str的第一个字符,所以可以把p作为字符串使用了。另一种可能是让p指向一个动态分配的字符串(➤17.2节)。

 char str[STR_LEN + 1], *p;
 p = str;

字符串的读和写

使用printf函数或puts函数来写字符串是很容易的。读字符串却有点麻烦,主要是因为输入的字符串可能比用来存储它的字符串变量长。为了一次性读入字符串,可以使用scanf函数或gets函数,也可以每次读入一个字符。

用printf函数和puts函数写字符串

 /*
  printf函数会逐个写字符串中的字符,直到遇到空字符才停止。(如果空字符丢失,printf函数会越过字符串     的末尾继续写,直到最终在内存的某个地方找到空字符为止。).
  puts函数总会添加一个额外的换行符,从而前进到下一个输出行的开始处。
 */
 #include <stdio.h>
 char str[] = "nothing to say";
 
 printf("%s\n", str);
 puts(str);


用scanf函数和gets函数读字符串

gets函数把读入的字符放到数组中,然后存储一个空字符。然而,在其他方面gets函数有些不同于scanf函数。·gets函数不会在开始读字符串之前跳过空白字符(scanf函数会跳过)。gets函数会持续读入直到找到换行符才停止(scanf函数会在任意空白字符处停止)。此外,gets函数会忽略掉换行符,不会把它存储到数组中,用空字符代替换行符。在把字符读入数组时,scanf函数和gets函数都无法检测数组何时被填满。因此,它们存储字符时可能越过数组的边界,这会导致未定义的行为。通过用转换说明[插图]s代替%s可以使scanf函数更安全。这里的数字[插图]指出可以存储的最多字符数。可惜的是,gets函数天生就是不安全的,fgets函数(➤22.5节)则是一种好得多的选择。


逐个字符读字符串

因为对许多程序而言,scanf函数和gets函数都有风险且不够灵活,C程序员经常会自己编写输入函数。通过每次一个字符的方式来读入字符串,这类函数可以提供比标准输入函数更大程度的控制。如果决定设计自己的输入函数,那么就需要考虑下面这些问题。·在开始存储字符串之前,函数应该跳过空白字符吗?·什么字符会导致函数停止读取:换行符、任意空白字符还是其他某种字符?需要存储这类字符还是忽略掉?·如果输入的字符串太长以致无法存储,那么函数应该做些什么:忽略额外的字符还是把它们留给下一次输入操作?

假定我们所需要的函数不会跳过空白字符,在第一个换行符(不存储到字符串中)处停止读取,并且忽略额外的字符。函数将有如下原型:

 int readLine(char str[], int n);

read_line函数主要由一个循环构成。只要str中还有空间,此循环就会调用getchar函数(➤7.3节)逐个读入字符并把它们存储到str中。在读入换行符时循环终止注意,ch的类型为int而不是char,这是因为getchar把它读取的字符作为int类型的值返回,

返回之前,read_line函数在字符串的末尾放置一个空字符。scanf和gets等标准函数会自动在输入字符串的末尾放置一个空字符;然而,如果要自己写输入函数,必须手工加上空字符。

 int readLine(char str[], int n){
  int ch, i;
     while (ch = getchar() != '\n')
         if (i < n)
             str[i++] = ch;
     str[i] = '\0';
     return i;
 }


访问字符串中的字符

字符串是以数组的方式存储的,因此可以使用下标来访问字符串中的字符。例如,为了对字符串s中的每个字符进行处理,可以设定一个循环来对计数器i进行自增操作,并通过表达式s[i]来选择字符。假定需要一个函数来统计字符串中空格的数量。利用数组取下标操作可以写出如下函数:

在s的声明中加上const表明count_spaces函数不会改变数组。如果s不是字符串,count_spaces将需要第2个参数来指明数组的长度。然而,因为s是字符串,所以count_spaces可以通过测试空字符来定位s的末尾。许多C程序员不会像例子中那样编写count_spaces函数,他们更愿意使用指针来跟踪字符串中的当前位置。就像在12.2节中见到的那样,这种方法对于处理数组来说一直有效,但在处理字符串方面尤其方便。下面用指针算术运算代替数组取下标来重新编写count_spaces函数。这次不再需要变量i,而是利用s自身来跟踪字符串中的位置。通过对s反复进行自增操作,count_spaces函数可以逐个访问字符串中的字符。下面是count_spaces函数的新版本:

注意,const没有阻止count_spaces函数对s的修改,它的作用是阻止函数改变s所指向的字符。而且,因为s是传递给count_spaces函数的指针的副本,所以对s进行自增操作不会影响原始的指针。


 int count_spaces(const char *s)
 {
   int count = 0;
   for (; *s != '\0'; s++)
     if (*s == ' ')
       count++;
   return count;
 }


使用C语言的字符串库

字符串惯用法

字符串数组(二维)

现在来看一个在使用字符串时经常遇到的问题:存储字符串数组的最佳方式是什么?最明显的解决方案是创建二维的字符数组,然后按照每行一个字符串的方式把字符串存储到数组中。考虑下面的例子:



算法(解决问题的步骤)练习题

问题步骤写清楚,搞清楚每个变量数据结构的含义,解决实际问题

链接语语法

树就是特殊的图,算法(思想)相似

经典算法《算法第四版》

单链表

  • 邻接表(多个单链表)存树or 图

  • 用数组下标关联e[]和ne[],只取下标正整数


ok的评论 (共 条)

分享到微博请遵守国家法律