C和指针复习系列二:第八章~第十五章

时间:2022-11-22 08:37:03

第八章: 数组

1. 看似简单,却有点恐怖的数组名

int b[ 4 ];
b的类型是什么?实际上我们并不能说b表示的是整个数组.

数组名的值是一个指针常量,也就是数组第一个元素的地址.它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是"指向int的常量指针";如果它们的其它类型,那么数组名的类型就是:"指向其他类型的常量指针".

但是数组和指针又不同,例如:数组具有确定数量的元素,而指针只是一个标量值.只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量,且指针常量是不可改变的.

只有在两种情况下,数组名并不用指针常量表示:当数组名作为sizeof操作符参数或者单目运算符&的操作数时.sizeof返回整个数组的长度,而不是指向数组的指针的长度.

sizeof( arr ) / sizeof( *arr );
而&表示取数组的地址:

int arr[ 2 ] = { 1, 2 };
int *pa = arr;
int *pa2 = &arr[ 0 ];


2. 数组和指针的少许讨论

指针和数组并不相等:

int a[ 5 ];
int *b;
声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置.声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间.而且,指针变量并未被初始化为指向任何现有的内存空间(一般先初始化为NULL).

所以*a是合法的,但是*b是非法的(如果为int *b = NULL,则合法).而b++可以通过编译,但是a++不行(毕竟a是指针常量,类似const,我们不能对const进行修改).

但是如何理解函数参数中的数组名呢?

#include <stdio.h>

void strcpy( char *buffer, char const *string )
{
while ( *buffer++ = *string++ ){
;
}
}

int main( void )
{
charbuffer[] = "hello";
charstring[] = "world";
printf("0x%x\n", buffer );
strcpy( buffer, string );
printf("%s\n0x%x\n", buffer, buffer );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

我们会发现,buffer的地址根本没被改变过,但是上面的值被改变了.对const进行复习,可以加深对上面例子的理解:

char  *const string =  "hello";
string[0] = 'a';
这是合法的,const修饰的是char*,表明指针所指向的内容不可被修改,所以下面代码有误:

char  *const string =  "hello";
string = "hello";
同样的道理,下面代码是正确的:

char  const *string =  "hello";
string = "hello";
而下面这段代码则有误:

char  const *string =  "hello";
string[0] = 'a';

3. 多维数组的少许讨论

1) 指向数组的指针

我们把多维数组当作一维数组对待,则下面的声明实际上是正确的,但是不太方便进行操作:

int arr[ 3 ][ 10 ];
int *p = arr;
例子如下:

#include <stdio.h>

int main( void )
{
intarr[ 3 ][ 10 ];
int*p = arr;
inti = 0;
intj = 0;
intk = 0;

for ( i = 0; i < 3; i++ ){
for ( j = 0; j < 10; j++ ){
arr[ i ][ j ] = k++;
}
}

printf("%d\n", *( p + 15 ) );

return 0;
}
把多维数组当作多维数组看待的话,则应该声明如下:

int arr[ 3 ][ 10 ];
int *p[ 10 ] = arr;
例子如下:

#include <stdio.h>

int main( void )
{
intarr[ 3 ][ 10 ];
int( *p )[ 10 ] = arr;
inti = 0;
intj = 0;
intk = 0;

for ( i = 0; i < 3; i++ ){
for ( j = 0; j < 10; j++ ){
arr[ i ][ j ] = k++;
}
}

//arr[ 1 ][ 5 ]
printf("%d\n", *( *( p + 1 ) + 5 ) );

return 0;
}

第九章:字符串,字符和字节
1. 关于字符串的几个函数

1) 字符串长度

#include <stdio.h>

size_t strlen( char const *str )
{
size_t len = 0;
while ( '\0' != *str++ ){
len++;
}

return len;
}

int main( void )
{
char *str = "hello world";
size_tlen = strlen( str );

printf("%d\n", len );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

2) 复制字符串

#include <stdio.h>

void strcpy( char *dst, char const *src )
{
while ( *dst++ = *src++ ){
;
}
}

int main( void )
{
char dst[ 100 ] = "hello world";
char src[] = "i love baobao forever.";

strcpy( dst, src );
printf("%s\n", dst );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

3) 连接字符串

#include <stdio.h>

void strcat( char *dst, char const *src )
{
while ( '\0' != *dst ){
dst++;
}
dst--;
while ( *dst++ = *src++ ){
;
}
}

int main( void )
{
char dst[ 100 ] = "i love";
char src[] = " baobao forever.";

strcat( dst, src );
printf("%s\n", dst );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

2. 字符操作

1) 字符分类

iscntrl任何控制字符
isspace空白字符
isdigit十进制数
isxdigit十六进制数
islower小写字母
isupper大写字母
isalpha字母
isalnum字母或数字
ispunct标点符号
isgraph任何图形文字
isprint任何可打印字符

2) 字符转换

tolower --- 转换为小写

toupper --- 转换为大写

3) 内存操作

void *memcpy( void *dst, void const *src, size_t length );
和strcpy类似,只是遇到NULL不会停止.

void *memmove( void *dst, void const *src, size_t length );
和memcpy类似,但dst和src内存重叠时照样成功复制(先复制到临时内存中)

void *memcmp( void const *a, void const *b, size_t length );
和strcmp类似
void *memchr( void const *a, int ch, size_t length );
和strchr类似
void *memset( void *a, int ch, size_t length );
将a中的前length数据初始化为ch。


第十一章:动态内存分配

1. 为什么使用动态分配内存

使用数组有以下缺点:

1) 使用数组引入了人为的限制,比如数组的大小实际上是确定的.

2) 如果确定了数组的大小,但实际上使用了较少的空间,则造成资源浪费.

3) 存在数组越界情况.


2. malloc,free,calloc和realloc

函数原型:

void *malloc( size_t size );
void free( void *pointer );
malloc的参数就是需要分配的内存字节数,如果成功,则返回一个指向被分配的内存块起始位置的指针.如果操作系统没有足够多的内存给malloc,则malloc返回NULL.因此,对每个从malloc返回的指针进行检查,确保它们并非NULL则非常的重要.

free的参数要么是NULL,要么是一个先前从malloc,calloc或realloc返回的值.向free传递一个NULL参数不会产生任何效果.

函数原型:

void *calloc( size_t num_elements, size_t element_size );
void *realloc( void *ptr, size_t new_size );
calloc也用于分配内存,和malloc的主要区别是后者在返回指向内存的指针之前把它初始化为0.calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值,它能够计算出总共需要分配的内存。

realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数,你可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,甚于部分内存的原先内容依然保留。

如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针。

最后,如果realloc函数的第一个参数是NULL,那么它的行为和malloc一模一样

(实际上并不赞同使用malloc和free,因为free太难写正确了,因为malloc后我们必须保存其指针,但是万一一不小心动了指针呢?)
例子:谁动了我的指针

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main( void )
{
int*parr = ( int * )malloc( sizeof( int ) * 5 );
int*tempParr = parr;
inti = 0;
for ( i = 0; i < 5; i++ ){
*parr++ = i;
}
tempParr++;//假设一不小心动了指针

free( tempParr );

return 0;
}
然后你懂的:

C和指针复习系列二:第八章~第十五章

3. 常见的动态内存错误

1) 动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define SIZE 100
int main( void )
{
int*parr = ( int * )malloc( sizeof( int ) * SIZE );

if ( NULL == parr ){
printf("out of memory\n");
return 1;
}
free( parr );

return 0;
}
当然,释放部分内存是错误的:

free( parr + 5 );


2)动态内存分配的第二大错误来源是操作内存时超出了分配内存的边界.如果超出,则引出两种类型的问题:

1---被访问的内存可能保持了其他变量的值,对它进行修改将破坏那个变量,修改那个变量将破坏存储在那里的值.

2---在malloc和free的有些实现中,它们以链表的形式维护可用的内存池.对分配的内存之外的区域进行访问可能破坏这个链表,这有可能产生异常,从而终止程序.

而当动态分配的内存不再需要使用时,它应该被释放,这样它以后可以被重新分配.如果不释放,则出现内存泄漏,导致可用内存越来越小,最终只能重启系统.


第十三章:高级指针话题

1. 进一步探讨指向指针的指针

#include <stdio.h>
#include <stdlib.h>

int main( void )
{
inti = 5;
int*pi = &i;
int**ppi;

ppi = π

//*ppi = pi;是基于ppi = &pi的,虽然实际上ppi = &pi可以推出*ppi = pi,
//但是*ppi = pi不能推出ppi = &pi.所以只写*ppi = pi,就等着内存报错吧.
*ppi = pi;

printf("%d\n", **ppi );

return 0;
}

2. 高级声明

int *f();
则f是一个函数,返回值类型是一个指向整型的指针.

int (*f)();
第一对括号的作用是:迫使间接访问在函数调用之前进行,使f成为一个函数指针(即我们必须传递函数的地址进去,进行*f后成为一个函数,然后调用这个函数),它所指向的函数返回一个整型值.

int *( *f )()
为函数指针,返回一个指向整型的指针.

int *f[];
[]的优先级高于*, 所以首先f是一个数组,数组内部的元素为*,而类型为int,所以数组的元素类型是指向整型的指针.

以下两个表达式均非法:

1) 

int f()[];
f是个函数,它返回一个整型数组---但是是非法,函数只能返回标量值,不能返回数组.我们可以这样理解:

void *ff = f();
int (*ff)[];
但实际上*ff不能为数组名(数组名是指针常量,是编译时期就确定的,而函数是动态时期执行的).

2) 

int f[]();
数组不能作为函数名.因为数组的长度是确定的,但是函数的长度确实不确定的.

所以下列的声明是正确的:

int *(*f[])();

3. 函数指针和回调函数

简单声明一个函数指针并不意味着它马上就可以使用.和其他指针一样,对函数执行间接访问之前必须把它初始化为指向某个函数:

int f( int );
int ( *pf )( int ) = &f;
而我们可以这样调用:

int ans;
ans = f( 25 );
ans = ( *pf )( 25 );
ans = pf( 25 );
第一条语句简单的使用名字调用函数f。但首先f被转换为函数指针,该指针指定函数在内存中的位置。然后,函数调用操作符调用该函数,执行开始于这个地址的代码。

第二条语句对pf执行间接访问操作,它把函数指针转换为一个函数名。但接着又被转换为函数指针来执行函数。

第三条语句直接调用函数指针来执行函数。


回调函数则是对函数指针的使用(用void*来实现C++中的template技术):

#include <stdio.h>
#include <stdlib.h>

//对一个数组进行查找
int search_list( int *arr, int value, int ( *compare )( void *, void * ), int len );
//查找int类型相等的函数
int compare_int( void *, void * );
//查找int类型大于第二个参数的函数
int compare_big_int( void *, void * );

int main( void )
{
intarr[ 5 ] = { 1, 2, 4, 5, 6 };
charstr[] = "hello world";
intresult = 0;

result = search_list( arr, 3, compare_big_int, 5 );
printf("%d\n", result );
result = search_list( arr, 4, compare_int, 5 );
printf("%d\n", result );

return 0;
}

int compare_int( void *a, void *b )
{
if ( *( int * )a == *( int * )b ){
return 1;
}

return 0;
}

int compare_big_int( void *a, void *b )
{
if ( *( int * )a > *( int * )b ){
return 1;
}

return 0;
}

int search_list( int *arr, int value, int ( *compare )( void *, void * ), int len )
{
inti = 0;
for ( i = 0; i < len; i++ ){
if ( compare( &arr[ i ], &value ) ){
return arr[ i ];
}
}

return -1;
}

程序输出:

C和指针复习系列二:第八章~第十五章

转移表的例子:

假设有个计算器程序:

switch ( oper ){
case ADD:
result = add( op1, op2 );
break;
case SUB:
result = sub( op1, op2 );
break;
case MUL:
result = mul( op1, op2 );
break;
case DIV:
result = div( op1, op2 );
break;
......
}

如果操作符很多,则switch则很长很长。

我们可以用函数指针来转换:

double add( double, double );
double sub( double, double );
double mul( double, double );
double div( double, double );
........
double ( *oper_func[] )( double, double ) = {
add, sub, mul, div,....
};

result = oper_func[ oper ]( op1, op2 );

4. 习题小汇总

1) 函数指针的巧妙使用

#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main( void )
{
int ( *func[] )( int value ) = {
iscntrl, isspace, isdigit, islower, isupper, ispunct, isprint
};
intn_count[ 8 ];
charch;
inti = 0;
memset( n_count, 0, sizeof( int ) * 8 );
while ( ( ch = getchar() ) != EOF ){
n_count[ 7 ]++;
for ( i = 0; i < 7; i++ ){
if ( func[ i ]( ch ) ){
n_count[ i ]++;
continue;
}
}
}

printf("cntrl:%.2f%%\n", n_count[0] * 100.0 / n_count[7] );
printf("space:%.2f%%\n", n_count[1] * 100.0 / n_count[7] );
printf("digit:%.2f%%\n", n_count[2] * 100.0 / n_count[7] );
printf("lower:%.2f%%\n", n_count[3] * 100.0 / n_count[7] );
printf("upper:%.2f%%\n", n_count[4] * 100.0 / n_count[7] );
printf("punct:%.2f%%\n", n_count[5] * 100.0 / n_count[7] );
printf("no print:%.2f%%\n", ( n_count[7] - n_count[6] ) * 100.0 / n_count[7] );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

2) sort函数的template版本

#include <stdio.h>

void sort( char *base, int len, int recsize, int ( *comp )( void *a, void *b ) );
void print_intarr( int *numArr, int len );
void print_chararr( char *str, int len );
void print_stringarr( char *arr[], int len );
intcompare_int( void *a, void *b );
intcompare_char( void *a, void *b );
intcompare_string( void *a, void *b );
void swap( char *i, char *j, int recsize );

int main( void )
{
intnumArr[ 10 ] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };
charchArr[] = "helloworld";
char*strArr[] = { "hello", "world", "i", "love", "baobao" };

sort( numArr, 10, 4, compare_int );
print_intarr( numArr, 10 );

sort( chArr, 11, 1, compare_char );
print_chararr( chArr, 11 );

sort( strArr, 5, 4, compare_string );
print_stringarr( strArr, 5 );

return 0;
}

intcompare_int( void *a, void *b )
{
return ( *( int * )a >= *( int * )b ) ? 1 : 0;
}

intcompare_char( void *a, void *b )
{
return ( *( char * )a > *( char * )b ) ? 1 : 0;
}

intcompare_string( void *a, void *b )
{
return ( *( char ** )a > *( char ** )b ) ? 1 : 0;
}

void print_intarr( int *numArr, int len )
{
int i = 0;
for ( i = 0; i < len; i++ ){
printf("%d ", numArr[ i ] );
}
printf("\n");
}

void print_chararr( char *str, int len )
{
inti = 0;
for ( i = 0; i < len; i++ ){
printf("%c ", str[ i ] );
}
printf("\n");
}

void print_stringarr( char *arr[], int len )
{
inti = 0;
for ( i = 0; i < len; i++ ){
printf("%s ", arr[ i ] );
}
printf("\n");
}

void swap( char *i, char *j, int recsize )
{
charx;
while ( recsize-- > 0 ){
x = *i;
*i++ = *j;
*j++ = x;
}
}

void sort( char *base, int len, int recsize, int ( *comp )( void *a, void *b ) )
{
char*i;
char*j;
char*last;

last = base + ( len - 1 ) * recsize;

for ( i = base; i < last; i+= recsize ){
for ( j = i + recsize; j <= last; j += recsize ){
if ( comp( i, j ) ){
swap( i, j, recsize );
}
}
}
}

程序输出:

C和指针复习系列二:第八章~第十五章


第十四章:预处理器

1. 预处理器的基本知识

C预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释,插入被#include指令包含的文件的内存,定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译.

2. #define与宏定义

1) #define后面为什么没有分号?

#define name stuff
每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff.注意:没有分号.如果有分号,则如果一些场合就要求一条语句,就会出现错误:

if ( ... )
name;
else
....

这样if后面是跟两条语句,直接出错.

2) 宏定义需要注意的地方

需要补齐所有的括号,所以

#define DOUBLE( a ) ( a ) + ( a )

是不正确的,考虑10 * DOUBLE( 5 )就知道了.
3) 宏定义的技巧

字符串自动拼接:

#include <stdio.h>

#define PRINT( FORMAT, VALUE ) printf("the value is " FORMAT "\n", VALUE )

int main( void )
{
PRINT( "%d", 3 );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

宏参数替换为一个字符串:#argument会被预处理器翻译成"argument"

#include <stdio.h>

#define PRINT( FORMAT, VALUE ) printf("the value of " #VALUE " is " FORMAT "\n", VALUE )

int main( void )
{
intx = 5;
PRINT( "%d", x + 3 );

return 0;
}
程序输出:

C和指针复习系列二:第八章~第十五章

##把位于它两边的符号连接成一个符号:

#include <stdio.h>

#define PRINT( FORMAT, VALUE ) printf("the value of " #VALUE " is " FORMAT "\n", VALUE##VALUE )

int main( void )
{
char*xx = "hello world";
PRINT("%s", x );

return 0;
}
程序输出:
C和指针复习系列二:第八章~第十五章


4) 宏与函数

宏非常频繁的用于执行简单的计算:

#define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )
为什么不用函数来完成这个任务呢?原因有两个:

1. 用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。

2. 更重要的是:函数的参数必须声明为一种特定类型,所以它只能在类型合适的表达式上使用。反之,宏可以应用于任何的数据类型。

但是宏相比于函数有以下缺点:

1. 每次使用宏的时候,都是将宏的代码拷贝到程序中去。如果宏的代码量大,则大幅度增加程序的长度。

还有一些用函数无法办到但是用宏可以办到:以下程序的宏的第二个参数是一种类型,它无法作为函数参数进行传递:

#define MALLOC( n, type ) ( ( type * )malloc( ( n ) * sizeof( type ) ) )
则:
pi = MALLOC( 25, int );
被替换为:

pi = ( ( int *) malloc( ( 25 ) * sizeof( int ) ) );
但宏也有副作用,所以除非简单的运算,否则尽量用函数来代替(C++就可以用inline函数):

#include <stdio.h>

#define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )

int main(void)
{
int x = 5;
int y = 8;
int z = 0;
z = MAX( x++, y++ );
printf("x = %d, y = %d, z = %d\n", x, y, z );

return 0;
}

程序输出:
C和指针复习系列二:第八章~第十五章



第十五章:输入/输出函数

1. perror函数用于报告错误:

void perror( char const *message )

C语言通常在Linux下很实用,当你包含了error.h的头文件后就会明白了,一堆的错误标志可供调用.


2. 文件与流I/O

1) FILE文件

FILE是一个数据结构,用于访问一个流.对于每个ANSI C程序,运行时系统必须提供至少三个流---标注输入,标准输出和标准错误.这些流的名字分别是stdin, stdout和stderr,它们都是一个指向FILE结构的指针.

2) 文件I/O的一般概况

1---程序为必须同时处于活动状态的每个文件声明一个指针变量,其类型为FILE*.这个指针指向这个FILE结构,当它处于活动状态时由流使用.

2---流通过调用fopen函数打开.为了打开一个流,你必须指定需要访问的文件或设备以及它们的访问方式.fopen和操作系统验证文件确实存在并初始化FILE结构.

3---然后,根据需要对该文件进行读取或写入.

4---最后,调用fclose函数关闭流.关闭一个流可以防止与它相关联的文件被再次访问,保证任何存储与缓冲区的数据被正确的写到文件中,并且释放FILE结构使它可以用于另外的文件.

打开文件流:fopen函数打开一个特定的文件,并把一个流和这个文件相关联

FILE *fopen( char const *name, char const *mode );

实例:

#include <stdio.h>

int main( void )
{
FILE *input;
input = fopen( "file.txt", "r" );
if ( NULL == input ){
perror("error:\n");
return 1;
}

return 0;
}

程序输出:

C和指针复习系列二:第八章~第十五章

关闭流:

int fclose( FILE *f );
成功返回0,否则返回EOF.


3. 字符I/O

基本函数如下:

int fgetc( FILE *stream );
int getc( FILE *stream );
int getchar( void );

int fputc( int character, FILE *stream );
int putc( int character, FILE *stream );
int putchar( int character );

int ungetc( int character, FILE *stream );

char *fgets( char *buffer, int buffer_size, FILE *stream );
char *gets( char *buffer );

int fputs( char const *buffer, FILE *stream );
int puts( char const *buffer );

int fscanf( FILE *stream, char const *format,... );
int scanf( char const *format,... );
int sscanf( char const *string, char const *format,... );

int fprintf( FILE *stream, char const *format,...);
int printf( char const *format,...);
int sprintf( char *buffer, char const *format,...);

size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t fwrite( void *buffer, size_t size, size_t count, FILE *stream );