C 程序设计语言——第五章笔记

1. 指针数组

参考:

c程序设计语言第二版英文版5.6 p108

书中一个程序例子,实现功能:
输入多行字符串,然后按照字母顺序排序,再打印出来。

解决思路:

  1. 需要一个指针数组存放输入的多行字符串:
char *lineptr[MAXLINES];

lineptr 是一个数组,大小为 MAXLINES,即最大存放的字符串的行数,数组的每个元素为指针,第一个元素指针指向输入的字符串第一行字符串的首字母地址。

用函数 readlines 来实现读取字符串,需要定义字符串的最大行数,每个字符串用 getline 来获取,定义每个字符串存放的最大字符数目,此外还需要空间存放字符串,因为数组存放的是指针。

  1. 对字符串排序,采用快速排序,比较大小用 strcmp函数。

  2. 将排序好的字符串打印输出

问题: 如何存放字符串
本书中用定义一个函数

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;
}

程序分析:

  1. 用argc != 1这个判断输入参数是否合法。
    如果输入参数没有 -x 或 -n 命令,那么合法形式参数只能两个,第一个为文件名,第二个字符串。因此退出 while 语句时argc 为1。大于1说明超过两个参数。
    如果参数有 -x, -n 命令,输入参数可能是三个或四个。最后推出argc一定为1。
    退出时 argc 的值说明还剩下几个参数,argv 指向模板字符串的地址。
  2. (*++argv)[0] 先将argv加1(第二个参数的地址),再用 * 取地址内容,即第二个参数,[0]表示取第二个参数(指针)指向字符串的第一个字符。如果为 - 号,则读取该字符后面的字符命令。
  3. 当参数以 - 号开头,读取后面字符的命令,此时 argv 是该字符串的地址,则通过 ** ++argv[0]* ,[] 的优先级最高,argv[0] 即为第二个字符串的地址,自加即将指向第二个字符串的指针加1,即将指针指向第二个字符串的第二个字符,最后取内容为第二个字符。判断,为 x 或 n 命令。
  4. 最终退出大的 while 循环后,argv 指向带检测的字符串。
  5. 用 getline 函数获取从键盘输入的字符串,*strstr(line, argv) != NULL 语句得到值 0 (输入字符串包含模板字符串),或 1(字符串不包含模板字符串),其结果与 except 的值(0表示不排除,1表示排除)不相同(包含字符串但不排除,不包含字符串且排除相同模式),则输出字符串。
  6. 因为自带有 getline 函数,这里区分自己写 mgetline(不保存换行符),第二个参数为读取最大字符(不包括换行符和空结束符),因此实际保存的字符串数组大小为 n + 1。

3. 指向函数的指针

快速排序程序

参考:

c 程序设计语言第二版英文版 p118

一个排序程序通常由三部分组成:

  1. 判断任何两个对象之间次序的比较操作
  2. 交换次序的交换操作
  3. 比较和交换对象直到所有对象都在正确位置的排序算法

由于排序算法和比较,交换操作是独立分开的,可以传递不同的比较和交换操作给排序程序来实现不同的排序标准。

例如:对字符串进行排序,根据输入不同,可能需要按照字母顺序排序,或者按照数值大小排序

这时,通过命令行参数选择比较方式,见下面程序:

#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语言

程序实现书中采用递归下降解析方法,该部分学了编译原理再补充。

发布了120 篇原创文章 · 获赞 2 · 访问量 5816

猜你喜欢

转载自blog.csdn.net/Lee567/article/details/102737037
今日推荐