C 程序设计语言第二版——第五章笔记
1. 指针数组
参考:
c程序设计语言第二版英文版5.6 p108
书中一个程序例子,实现功能:
输入多行字符串,然后按照字母顺序排序,再打印出来。
解决思路:
- 需要一个指针数组存放输入的多行字符串:
char *lineptr[MAXLINES];
lineptr 是一个数组,大小为 MAXLINES,即最大存放的字符串的行数,数组的每个元素为指针,第一个元素指针指向输入的字符串第一行字符串的首字母地址。
用函数 readlines 来实现读取字符串,需要定义字符串的最大行数,每个字符串用 getline 来获取,定义每个字符串存放的最大字符数目,此外还需要空间存放字符串,因为数组存放的是指针。
-
对字符串排序,采用快速排序,比较大小用 strcmp函数。
-
将排序好的字符串打印输出
问题: 如何存放字符串
本书中用定义一个函数
char *alloc(int);
来分配空间,功能类似 malloc,第一次写程序时用书中的思路用 malloc来分配空间,但该函数在子函数 readlines 中,程序如下:
int readlines(char *lineptr[], int maxlines)
{
int nlines, len;
nlines = 0;
char *p, line[MAXLEN];
while ((len = mgetline(line, MAXLEN-1)) > 0 && nlines < maxlines) {
p = (char *) malloc((len+1) * sizeof(char));
strcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
如上,子函数中建立一个指针变量和一个数组,分别临时存放字符串地址和字符串内容,每读取一行数据存放,再用 malloc 分配空间存放字符串,最后将分配的空间地址赋给指针数组。
问题
当离开子函数 readlines 后,malloc 分配的内存空间是否会释放,lineptr 存放的是字符串的地址,离开子函数后变量 p 和 line 都会不存在,如果 malloc 分配的空间还是存放的字符串,那怎么将其释放,等程序结束后系统系统自动释放?
内存问题学习深入理解计算机系统后补充。
下面是完整程序:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXLINES 5000
#define MAXLEN 100
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void mqsort(char *lineptr[], int left, int right);
int main(void)
{
int nlines;
char *lineptr[MAXLINES];
printf("Enter strings (empty line to stop):\n");
if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
mqsort(lineptr, 0, nlines-1);
printf("Sorted strings:\n");
writelines(lineptr, nlines);
return 0;
}
else {
printf("Error: no input\n");
return 1;
}
}
int mgetline(char *s, int n)
{
int i, c;
for (i = 0; i < n && (c = getchar()) != EOF && c != '\n'; i++)
*(s+i) = c;
*(s+i) = '\0';
return i;
}
int readlines(char *lineptr[], int maxlines)
{
int nlines, len;
nlines = 0;
char *p, line[MAXLEN];
while ((len = mgetline(line, MAXLEN-1)) > 0 && nlines < maxlines) {
//子函数分配内存,离开后分配的内存空间并没有释放,因为lineptr里指针指向的空间字符串还在
//学了操作系统再补充
p = (char *) malloc((len+1) * sizeof(char));
strcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
//快速排序
typedef char *ElementType;
#define Cutoff 3
void swap(ElementType *s1, ElementType *s2) //参数为双重指针,交换指向字符串的指针
{
ElementType t;
t = *s1;
*s1 = *s2;
*s2 = t;
}
//选择基准元素
ElementType Median3(ElementType a[], int left, int right)
{
int center = (left + right) / 2;
if (strcmp(a[left], a[center]) > 0)
swap(&a[left], &a[center]);
if (strcmp(a[left], a[right]) > 0)
swap(&a[left], &a[right]);
if (strcmp(a[center], a[right]) > 0)
swap(&a[center], &a[right]);
swap(&a[center], &a[right-1]);
return a[right-1]; //return pivot
}
//插入排序
void InsertionSort(ElementType a[], int n)
{
int i, p;
ElementType tmp;
for (p = 1; p < n; p++) {
tmp = a[p];
for (i = p; i > 0 && strcmp(a[i-1], tmp) > 0; i--)
a[i] = a[i-1];
a[i] = tmp;
}
}
//快速排序
void mqsort(char *a[], int left, int right)
{
int i, j;
ElementType pivot;
if ( left + Cutoff <= right) {
pivot = Median3(a, left, right);
i = left;
j = right - 1;
for ( ; ; ) {
while (strcmp(a[++i], pivot) < 0)
;
while (strcmp(a[--j], pivot) > 0)
;
if (i < j)
swap(&a[i],&a[j]);
else
break;
}
swap(&a[i], &a[right-1]);
mqsort(a, left, i-1);
mqsort(a, i+1, right);
}
else
InsertionSort(a+left, right-left+1);
}
//writelines
void writelines(char *lineptr[], int nlines)
{
int i;
for (i = 0; i < nlines; i++)
puts(lineptr[i]);
}
程序正常输出,内存快并未被释放:
Enter strings (empty line to stop):
sdjf
aslk
vdke
rsd
sa
sg
wa
ao
Sorted strings:
ao
aslk
rsd
sa
sdjf
sg
vdke
wa
书中习题 5.7 需要对此程序更改,将字符串存在主函数定义的数组中。
程序见:
C 程序设计语言——第五章练习题
解决:子函数中用 malloc 函数分配的空间如果没有用 free() 函数释放,则退出子函数后仍存在。
2. 命令行参数
参考:
c程序设计语言第二版英文版5.10 p117 程序例子
程序实现如下命令功能:
find -x -n pattern
或
find -xn pattern
书中程序代码:
#include<stdio.h>
#include<string.h>
#define MAXLEN 1000
int mgetline(char *line, int max);
int main(int argc, char *argv[])
{
char line[MAXLEN];
long lineno = 0;
int c, except = 0, number = 0; found = 0;
while (--argc > 0 && (*++argv)[0] == '-')
while (c = *++argv[0])
switch (c) {
case 'x':
except = 1;
break;
case 'n':
number = 1;
break;
default:
printf("Usage: find -x -n pattern.\n");
argc = 0;
found = -1;
break;
}
if (arc != 1)
printf("Usage: fiind -x -n pattern.\n");
else
while (mgetline(line, MAXLEN-1) > 0) {
lineno++;
if ((strstr(line, *argv) != NULL) != except) {
if (number)
printf("%ld :",lineno);
printf("%s\n",line);
found++;
}
}
return found;
}
int mgetline(char *s, int n)
{
int i, c;
for (i = 0; i < n && (c = getchar()) != EOF && c != '\n'; i++)
s[i] = c;
s[i] = '\0';
return i;
}
程序分析:
- 用argc != 1这个判断输入参数是否合法。
如果输入参数没有 -x 或 -n 命令,那么合法形式参数只能两个,第一个为文件名,第二个字符串。因此退出 while 语句时argc 为1。大于1说明超过两个参数。
如果参数有 -x, -n 命令,输入参数可能是三个或四个。最后推出argc一定为1。
退出时 argc 的值说明还剩下几个参数,argv 指向模板字符串的地址。 - (*++argv)[0] 先将argv加1(第二个参数的地址),再用 * 取地址内容,即第二个参数,[0]表示取第二个参数(指针)指向字符串的第一个字符。如果为 - 号,则读取该字符后面的字符命令。
- 当参数以 - 号开头,读取后面字符的命令,此时 argv 是该字符串的地址,则通过 ** ++argv[0]* ,[] 的优先级最高,argv[0] 即为第二个字符串的地址,自加即将指向第二个字符串的指针加1,即将指针指向第二个字符串的第二个字符,最后取内容为第二个字符。判断,为 x 或 n 命令。
- 最终退出大的 while 循环后,argv 指向带检测的字符串。
- 用 getline 函数获取从键盘输入的字符串,*strstr(line, argv) != NULL 语句得到值 0 (输入字符串包含模板字符串),或 1(字符串不包含模板字符串),其结果与 except 的值(0表示不排除,1表示排除)不相同(包含字符串但不排除,不包含字符串且排除相同模式),则输出字符串。
- 因为自带有 getline 函数,这里区分自己写 mgetline(不保存换行符),第二个参数为读取最大字符(不包括换行符和空结束符),因此实际保存的字符串数组大小为 n + 1。
3. 指向函数的指针
快速排序程序
参考:
c 程序设计语言第二版英文版 p118
一个排序程序通常由三部分组成:
- 判断任何两个对象之间次序的比较操作
- 交换次序的交换操作
- 比较和交换对象直到所有对象都在正确位置的排序算法
由于排序算法和比较,交换操作是独立分开的,可以传递不同的比较和交换操作给排序程序来实现不同的排序标准。
例如:对字符串进行排序,根据输入不同,可能需要按照字母顺序排序,或者按照数值大小排序
这时,通过命令行参数选择比较方式,见下面程序:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXLINES 100
#define MAXLEN 100
int mgetline(char *s, int n);
void writelines(char *v[], int n);
int numcmp(char *s1, char *s2);
int mstrcmp(char *s1, char *s2);
void mqsort(void *v[], int left, int right, int (*comp)(void*, void*));
int main(int argc, char *argv[])
{
char *lineptr[MAXLINES];
int nlines = 0;
int len;
char line[MAXLEN+1];
//检查输入参数
int numeric = 0;
if (argc == 2 && strcmp(argv[1], "-n") == 0)
numeric = 1;
//获取输入字符串,一行字符串超过会去掉多余的
printf("Enter strings (maximum lines : %d, "
"maximum columns : %d):\n",MAXLINES, MAXLEN);
while (nlines < MAXLINES && (len = mgetline(line, MAXLEN)) > 0) {
lineptr[nlines] = (char *)malloc((len+1) * sizeof(char));
strcpy(lineptr[nlines++], line);
}
if (nlines == 0) {
puts("No input\n");
return 1;
}
//打印原始字符串
printf("\nOriginal strings:\n");
writelines(lineptr, nlines);
//快速排序
puts("\nSorted strings:");
mqsort((void **)lineptr, 0, nlines-1,(int (*)(void *, void*))(numeric ?
numcmp : mstrcmp));
writelines(lineptr, nlines);
//释放内存
for (int i = 0; i < nlines; i++)
free (lineptr[i]);
return 0;
}
int mgetline(char *s, int n)
{
int i, c;
for (i = 0; i < n && (c = getchar()) != EOF && c != '\n'; i++)
s[i] = c;
s[i] = '\0';
if (c != '\n' && c != EOF)
while (getchar() != '\n')
continue;
return i;
}
//快速排序
void swap(void *v[], int i, int j)
{
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
int numcmp(char *s1, char *s2)
{
double v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if (v1 < v2)
return -1;
else if (v1 == v2)
return 0;
else
return 1;
}
int mstrcmp(char *s1, char *s2)
{
int i;
for (i = 0; s1[i] == s2[i]; i++)
if (s1[i] == '\0')
return 0;
return s1[i] - s2[i];
}
void InsertionSort(void *v[], int nlines, int (*comp)(void*, void*))
{
int i, p;
char tmp[MAXLEN+1];
for (p = 1; p < nlines; p++) {
strcpy(tmp, v[p]);
for (i = p; i > 0 && (*comp)(v[i-1], tmp) > 0; i--)
strcpy(v[i], v[i-1]);
strcpy(v[i], tmp);
}
}
void *Median3(void *v[], int left, int right, int (*comp)(void*, void*))
{
int center = (left + right) / 2;
if ((*comp)(v[left], v[center]) > 0)
swap(v, left, center);
if ((*comp)(v[left], v[right]) > 0)
swap(v, left, right);
if ((*comp)(v[center], v[right]) > 0)
swap(v, center, right);
swap(v, center, right-1);
return v[right-1];
}
#define cutoff 3
void mqsort(void *v[], int left, int right, int (*comp)(void *, void*)) {
void *pivot;
int i, j;
if (left + cutoff <= right) {
pivot = Median3(v, left, right, comp);
i = left;
j = right - 1;
for (; ; ) {
while ((*comp)(v[++i], pivot) < 0)
;
while ((*comp)(v[--j], pivot) > 0)
;
if (i < j)
swap(v, i, j);
else
break;
}
swap(v, i, right-1);
mqsort(v, left, i-1, comp);
mqsort(v, i+1, right, comp);
}
else
InsertionSort(v+left, right-left+1, comp);
}
//打印字符串
void writelines(char *v[], int nlines)
{
for (int i = 0; i < nlines; i++)
puts(v[i]);
}
程序分析
快速排序算法
参考:
数据结构与算法分析 c语言描述 英文版第二版 p235
快速排序你真的会了吗?
1. 选择基准值
1)选择第一个元素或最后一个元素
缺点: 如果元素已经排序好或以逆序排布,则元素每次分割都会分到一边,另一边没有数据,这样浪费了大量的时间做无用的事。这样最差的时间复杂度为 O(N^2)。因此,这种方法不可取。
2) 随机选取
ElementType randomPivot(ElementType a[], int left, int right)
{
srand((unsigned) time(NULL));
int randIndex = rand() % (right - left + 1) + left;
return a[randIndex];
}
3) 三数取中值
在数列中选取最左边,中间,最右边三个数进行排序,排序好后的中间的值作为基准值,因为最右边数比基准值大,将基准值与倒数第二个数交换位置,见下面程序:
ElementType Median3(ElementType a[], int left, int right)
{
int center = (left + right) / 2;
if (a[left] > a[center])
swap(&a[left], &a[center]);
if (a[left] > a[right])
swap(&a[left], &a[right]);
if (a[center] > a[right])
swap(&a[center], &a[right]);
swap(&a[center], &a[right-1]);
return a[right-1];
}
2. 分割策略
问题: 左边的数与基准值比较时遇到与基准值相等的数是否交换?
考虑情形: 所有元素均相等,如果遇到和基准值相等的数仍继续扫描,则最终会导致数列不会分成两部分,和排好序而基准值选第一个元素的情况一样,所有元素被分到一边。
而如果遇到相等元等元素时停止交换,尽管看上去做了无用的交换,但最终基准值会处于中间位置,将数列分成两部分。这种时间复杂度为 O(N logN)。
因此,选取第二种方式。
3. 小数列排序
对于小数列 (N <= 20)时,采用插入排序比快速排序好。
4. 代码分析
#define cutoff 3
void Qsort(ElementType a[], int left, int right)
{
int i, j;
ElementType pivot;
if (left + cutoff <= right) {
pivot = Median3(a, left, right);
i = left;
j = right-1;
for (; ; ) {
while (a[++i] < pivot) ;
while (a[--j] > pivot) ;
if (i < j)
swap (&a[i], &a[j]);
else
break;
}
swap (&a[i], &a[right-1]);
Qsort(a, left, i-1);
Qsort(a, i+1, right);
}
else
InsertionSort(a+left, left-right+1);
}
分析:
代码
while (a[++i] < pivot) ;
while (a[--j] > pivot) ;
显示了快速排序的优势,算法内部的循环只有自赠,自减,测试。如果该部分改成:
i = left + 1;
j = right - 2;
for (; ; ) {
while (a[i] < pivot)
i++;
while (a[j] > pivot)
j--;
if (i < j)
swap (&a[i], &a[j]);
else
break;
}
则当 a[i] = a[j] = pivot 时会出错,因为程序会无限交换且不会往后移动。
本实例中的程序
本例子中输入字符串,排序的是字符串,快速排序算法思想不变,具体程序代码进行修改:
void mqsort(void *v[], int left, int right, int (*comp)(void*, void*));
第一个参数是一个指针数组,存放的是指向字符串的指针。
第四个元素 comp 是指向函数的指针。函数的参数是 void * 和 void *,返回值是 int。
(参考:C程序设计语言(K&R)笔记第21问题)
关于:
mqsort((void **)lineptr, 0, nlines-1,(int (*)(void *, void*))(numeric ? numcmp : mstrcmp));
参考:
(int (*)(void *, void *))(numeric ? numcmp : strcmp) ?
void * 是通用指针类型,任何类型的指针都可以转换为 void *类型,并且在将它转换回原来的类型时不会丢失信息。
注意:
while ((*comp)(v[++i], pivot) < 0)
这里调用函数采用的是 (*comp)(v[++i], pivot) 形式,因为 comp 是指向函数的指针,*comp 则是函数,因此采用这种形式。
但参考:
c primer plus 第六版英文版 14.14 p659
另一种形式 comp (v[++i], pivot) 也可以,这种形式的理由是,函数名是指针,因此可以和指针互换使用。
?4. 复杂的声明
参考书:
c 程序设计语言第二版英文版 第五章 5.12 p122
c 程序设计语言第二版英文版 参考手册 A8 p210
声明(declaration)用于说明每个标识符的含义,而并不需要为每个标识符预留存储空间。
预留存储空间的声明称为定义(definition)。
声明的形式如下:
declaration:
declaration-specifiers init-declarator-listopt;
4.1 声明说明符
第一项是声明说明符,包括:
declaration-specifiers:
storage-class-specifier declaration-specifiersopt
type-specifier declaration-specifiersopt
type-qualifier declaration-specifiersopt
存储类别说明符:
storage-class specifier:
auto
register
static
extern
typedef
类型说明符:
type specifier:
void
char
short
int
long
float
double
signed
unsigned
struct-or-union-specifier
enum-specifier
typedef-name
类型限定符:
type-qualifier:
const
volatile
4.2 声明器
参考博客:
The C ProgrammingLanguage 5.12复杂声明
这里说简化的形式,详细的见参考手册A8.5
dcl: optional *'s direct-dcl
direct-dcl name
(dcl)
direct-dcl()
direct-dcl[optional size]
声明符 dcl 就是前面可能带有多个*的 direct-dcl。
direct-dcl 可以是 name、由一对圆括号括起来的 dcl、后面跟有一对圆括号的 direct-dcl、后面跟有用方括号括起来的表示可选长度的 direct-dcl。
解析复杂的声明:
c语言中复杂声明的解析
复杂声明 | C语言
程序实现书中采用递归下降解析方法,该部分学了编译原理再补充。