重读经典:C和指针学习笔记

重读经典:《C和指针》学习笔记

《C和指针》、 《C专家编程》、 《C陷阱和缺陷》并称c语言三本经典著作,笔者在许多年前囫囵吞枣读完了这三本经典,然后把这三本书束之高阁。时至今日,大部分内容都已忘记,前些时间偶然翻出来,重读这些经典,顺便做做笔记,记录其精华。
《C和指针》通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。

第1章 快速上手

本章讲的是C语言的基础知识,目的是使读者对C语言有一个整体的初步的认识。
这一章给读者一些忠告,包括:

  • 使用/*和*/进行块注释时,需要注意块中是否包含有*/这样的结束符号,还有一种块注释的方法,就是使用#ifdef预编译命令
  • 编程时注意=和==、&和&&的区别,不要漏写或者多写
  • 在调用scanf函数时,传入的参数为指针类型,不要忘记写&
  • 使用#include指令可以避免重复声明,同时减少代码量
  • getchar()函数的返回值是整数而不是字符类型(《C陷阱和缺陷》也强调这个问题)

第2章 基本概念

第二章仍然讲C语言的基础知识,主要知识点包括:

1、程序的编译链接环境和运行环境可以有所不同;

程序的编译和链接,编译是将源文件翻译成目标文件,包括对define和include等预处理器的替换;链接是将目标文件和所需要的库文件生成可执行文件;

2、标识符

  1. 对于标识符的长度,ANSI标准规定外部标识符长度至少能由前6个字符唯一区分,并且不区分大小写,内部标识符长度至少能由前31个字符唯一区分。为保证代码的可移植性,最好严格遵守这一标准。(《C陷阱和缺陷》也强调过这个问题)
  2. 当然,程序猿定义的标识符是不能与C语言保留关键字冲突的。

3、字符转义

C中定义了一些字母前加”\”来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,都不是它本来的ASCII字符意思了。
C语言中转义字符有两种表示方式,一种是在字符前加”\”,还有一种是三字母词。大多数程序猿只了解第一种,对第二种并不熟悉。

三字母词就是几个字符的序列,合起来表示另一个字符,比如

三字母词 含义
??( [
??< {
??= #
??) ]
??> }
??/ \
??!
??’ ^
??- ~

“\”转义
部分转义字符定义:

转义字符 含义
\a 警告字符,可能会奏响铃声或者产生一些其它可见字符
\b 退格
\f 进纸
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd 表示1-3个八进制数字,给定的八进制数转义为对应的ASCII字符
\xddd 表示1-3个十六进制数字,给定的十六进制数转义为对应的ASCII字符,这个值大小可能超出范围

注:
1,\v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
2,\n其实应该叫回车换行。换行只是换一行,不改变光标的横坐标;回车只是回到行首,不改变光标的纵坐标。
3,\t 光标向前移动四格或八格。
4,\b是退格键,往前移动光标,不删除字符,但该字符可能会被后续字符覆盖。
5,这些都是从电传打字机沿用下来的。

4、编程风格

C语言是一种自由形式的语言,语法较为宽松,但良好的程序风格和文档将使代码更容易阅读和维护。

第3章 数据

第3章仍然讲C语言的基础知识,主要讲数据类型的定义。

1、C语言数据类型

C语言的数据类型包括基本类型、聚合类型和指针类型,基本类型主要包括int,char,float,double,聚合类型包括结构体、数组、枚举和联合,具体类型见下图:

重读经典:C和指针学习笔记_第1张图片

其中有些细节需要注意:
ANSI标准规定:长整型至少应该和整型一样长,整型至少应该和短整型一样长。标准同时规定长整型的长度至少为32位,短整型至少为16位,对于整型的长度并没有明确规定。究竟整型数的长度是32位还是16位或者是64位,取决于编译器。当然,编译器可以把这三种类型的长度都设置为32位。
设计char类型的初衷是为了容纳字符类型,但其本质是8位的整型数据。至于缺省的char类型是signed还是unsigned,由编译器决定。为确保可移植性,char类型变量应该位于signed和unsigned的交集,即ascii字符集中。

2、指针类型

int *a;
int* a;
这两种定义的结果是一样的,都是将a定义为一个int类型的指针。
但是,如果要定义三个指针,正确的方式是:
int *a,*b,*c;而不是int* a,b,c;

3、常量

常量使用const修饰,常量在运行过程中不可修改,初始化一个常量由两种方法,
一种是在定义的时候进行初始化;
另一种是在函数调用的时候,声明为const类型的形参将被初始化为实参

int * const p;这种定义,p是常量,指针的值不可改变,但指针指向的内容可以改变;
int const * a;这种定义,*p是常量,指针的值可改变,但指针指向的内容不可改变;
这两种定义的含义不同

4、作用域

一个标识符的作用域有四种类型:
- 文件作用域:
- 代码块作用域:
- 函数作用域:goto语句后的标识符
- 原型作用域:函数声明中形参名字

5、static关键字

static关键字有两种用法,
修饰全局变量和函数时,改变其链接属性,文件外部不可访问;
修饰局部变量时,改变其存储类型,由栈空间存储变成全局空间存储;
在C++中,static可以将类的成员变量变成静态成员变量。

6、隐式类型转换

进行算术运算(加、减、乘、除、取余以及符号运算)时,不同类型数招必须转换成同 一类型的数据才能运算,算术转换原则为:

整型提升:对于所有比int小的类型,包括char, signed char, unsigned char, short, unsigned short,首先会提升为int类型。

在进行运算时,以表达式中最长类型为主,将其他类型位据均转换成该类型,如:

(1)若运算数中有double型或float型,则其他类型数据均转换成double类型进行运算。

(2)若运算数中最长的类型为long型.则其他类型数均转换成long型数。

(3)若运算数中最长类型为int型,则char型也转换成int型进行运算。算术转换是在运算过程中自动完成的。

(4)若运算数中最长类型包含signed与unsigned int,signed会转换为unsigned。

(5)若运算数中最长类型包含signed long int 与unsigned long int,signed会转换为unsigned。

(6)若运算数中最长类型包含float 与double,float会转换为double。

(7)若运算数中最长类型包含double与long double,double 会转换为long double。

(8)C语言中算术运算总是以缺省整型的精度进行运算,表达式中的字符类型和短整型在使用前进行整型提升,比如
char a,b,c;
a=b+c;
b和c被提升为整型,然后执行加法运算,运算结果被截断,赋值给c

进行赋值操作时,赋值运算符右边的数据类型必须转换成赋值号左边的类型,若右边的数据类型的长度大于左边,则要进行截断或舍入操作。

注意:在进行自动类型转换的时候,如果原来的数是无符号数,那么在扩展的时候,高位填充的是0;如果是有符号数,那么高位填充的时符号位。扩展时与转换后的数的数据类型无关。
例子:

unsigned char c=0xff;
int a;
unsigned int b;
a=c;
b=c;
运行结果:a=0xff,b=0xff

char c=0xff;
int a;
unsigned int b;
a=c;
b=c;
运行结果:a=0xffffffff,b=0xffffffff

第4章 语句

第4章仍然讲C语言的基础知识,主要讲语句。

1、printf返回值

printf函数其实是有返回值的,msdn是这么说的:
Return Values
Each of these functions returns the number of characters printed, or a negative value if an error occurs.
返回值是打印字符的个数。

其实sprintf函数也有返回值,返回值是写入buffer中的字节数。

2、for语句

for(expression1,expression2,expression3)
statement;
expression1:初始化部分,在循环体执行前执行一次
expression2:条件部分,在循环体每次执行前执行
expression3:调整部分,在循环体执行完毕、条件部分每次执行前执行
for语句中可以使用break语句和continue语句。

3、switch语句

每个case标签必须具有一个唯一的常量值或常量表达式,常量表达式可以在编译期间进行计算。
switch语句中可以使用continue语句,但没有任何效果。
再次强调,case语句后不要漏写break语句。

第5章 操作符和表达式

1、移位

ANSI标准说明无符号数的移位都是逻辑移位,有符号数的移位究竟是逻辑移位还是算术移位由编译器决定。
算术移位将保留操作数的符号位。

2、表达式

赋值运算符从右向左的结合顺序,x=y=a+3;会被编译器翻译成两条语句,y=a+3;x=y;这样写是合法的。但是赋值运算符的计算顺序ANSI标准并未规定,编译器可以先计算运算符左侧表达式的值,也可以先计算运算符右侧表达式的值。因此,*p++=*p++;这样的语句使用不同的编译器可能得到不同的结果。
逗号可以把多条语句合成一条,比如

while(x<y)
    x=2,y=3;

这样写是合法的,但这并不是好的写法。

数组的下标引用和间接访问表达式是等价的,a[下标]=*(a+下标);

3、布尔变量

C语言并不具备显示的布尔累心个,所以用整数代替布尔类型:0是假,任何非0皆为真,”1”这个值并不比其它非零值更真。

4、左值和右值

左值和右值
左值:能够出现在赋值运算符左侧的东西;
右值:能够出现在赋值运算符右侧的东西;
左值可以成为右值,右值缺并不一定能成为左值。
变量可以成为左值,包含下标引用和间接访问的表达式可以成为左值。

第6章 指针

指针是C语言的精华之所在,具有强大的灵活性,也是最容易出问题的地方。对于初学者而言,指针难以理解和掌握。即使对于有经验的程序猿,用好指针也不是一件容易的事。对指针的掌握程度已经程序检验一个程序猿的试金石。
指针的本质是一个变量,这个变量中保存的值被解释为内存地址。这个内存地址可以指向不同的数据类型,比如指向char类型,int类型或者float类型甚至指向指针类型。
对于固定架构的cpu来说,指针变量的长度是固定的,在x86计算机上,指针变量的长度是4。这也是指针强大的灵活性的基础,也是指针强制类型转换的基础。
对于cpu而言,在内存地址中存储的数据都是0、1这样的二进制位,至于长度是1个字节、2个字节或者4个字节,完全由程序开发者决定。比如:

int *a=100;
char *b=(char*)a;//a和c指向同一个内存地址,在解引用时*a访问的是4个字节,*c访问的是一个字节
int c;
char d;
c=*a;  //对应汇编语言 mov c,DWORD PTR [a]
d=*b;  //对应汇编语言 movsb b,DWORD PTR [b]

指针可以进行算术运算。
指针可以加上一个整数,加上一个整数之后,指针的值会移动适当长度,比如:

int *a,*b;
b=a;
a+=1;//此时b-a=sizeof(int)

两个指向相同数据类型的指针可以进行减法运算。程序猿应当确保这两个指针指向同一数组或者由malloc申请的内存中,否则结果没有实际意义。
指针在使用前应进行初始化并判断是否为空指针。解引用空指针在大部分操作系统中会造成程序崩溃,如果程序不崩溃,将导致一个潜在的bug。
声明一个指针并不会自动分配相应的内存。
再次强调,*p++等同于*(p++)。

第7章 函数

1、函数定义和函数声明

函数定义即函数体,函数体在代码块中实现。函数可以有返回值,也可以没有返回值,返回值也可以被丢弃。
函数声明提供给编译器使用,提供了参数个数以及参数类型这些信息,编译期间检查函数调用传入的参数个数及每个参数的类型与函数声明是否严格匹配,不一致则报错。
函数声明的作用域与变量的作用域一样,可以是文件作用域,也可以是代码块作用域。为了防止函数声明的多个拷贝出现不一致的情况,通常把函数原型的声明放在一个头文件中,使用include函数包含这个头文件,如果修改函数声明的话,修改一处即可。这样可以提高代码的可维护性和编程效率。

2、函数参数

函数调用时参数传递有两种方式,

传值调用:被调用函数将获得实参的一份拷贝,对参数的读写操作都是对拷贝的操作,因此,形参不能改变实参。查看反汇编代码,可以看到参数传递过程用栈实现,用push语句将实参入栈,这样实参和形参保存在不同的内存地址。

传址调用:顾名思义,传入的是变量的地址,用变量的地址对变量进行解引用,就可以修改实参的值。如果传入的参数是一个数据名,进行数组名的拷贝,按道理讲数组名应该代表整个数组,但实际上数组名代表的是数据第一个元素的地址,与指针等价,因此传递数组参数时并不会进行数据拷贝,退化为传址调用。

3、递归

递归函数:在函数内部直接或者间接调用了自身的函数。
在理解了递归函数之后,阅读递归函数最容易的方法不是纠缠于其执行过程,而是相信递归函数会顺利完成任务。
教科书中常用两个例子介绍递归函数,分别是计算阶乘和斐波那契序列,用递归函数实现的代码如下:

unsigned int factorial(unsigned int n)
{
    if (n==0 || n==1)
    {
        return 1;
    }
    return n*factorial(n-1);
}

unsigned int fibonacci(unsigned int n)
{
    if (n==0)
    {
        return 0;
    }
    else if (n==1)
    {
        return 1;
    }
    return fibonacci(n-1)+fibonacci(n-2);
}

在计算阶乘时,使用递归并未有任何优越之处;使用递归计算斐波那契数列则造成大量的重复计算,效率低下。
《C和指针》列举了一个非常有意思的例子,把一个整数值转化为字符串打印出来
思路是把这个整数值除以10,把余数加上0x30,依次得到从个位到最高位的字符,但这样打印出来就会倒序显示,使用递归可以解决这个问题,为了简单起见,假设输入的数据是无符号数

void myitoa(unsigned int n)
{
    unsigned int quotient;
    quotient=n/10;

    if (quotient)
    {
        myitoa(quotient);
    }
    printf("%d",n%10);
}

尾部递归:如果递归函数体内执行的最后一条语句是调用自身,这个函数就被称为尾部递归。
尾部递归都可以转化为用循环实现,效率通常更高一些。因为大部分编译器都通过运行时栈实现递归函数,与循环实现的方法相比,递归调用会占用大量的栈空间。
计算阶乘和斐波那契序列的两个递归函数就是尾部递归,用循环实现的代码如下:

unsigned int factorial(unsigned int n)
{
    unsigned int result=1;
    if (n==0 || n==1)
    {
        return 1;
    }
    while(n)
    {
        result*=n--;
    }
    return result;
}

unsigned int fibonacci(unsigned int n)
{
    unsigned int result=0;
    unsigned int prev,prevprev,i;
    if (n==0)
    {
        return 0;
    }
    else if (n==1)
    {
        return 1;
    }
    prev=1;
    prevprev=0;
    i=2;
    while(i++<=n)
    {
        result=prev+prevprev;
        prevprev=prev;
        prev=result;
    }
    return result;
}

第8章 数组

1、数组名的本质

数组名代表什么?合乎逻辑的答案是代表整个数组,事实并非如此,在C语言中,数组名代表的是一个指针常量,也就是第一个元素的地址。

2、数组与指针的区别

  • 当数组名在表达式中使用时,编译器会为它产生一个指针常量,注意是指针常量。所以数组名不能作为左值,这是数组名与指针的区别之一。
  • 数组名用于sizeof操作符时,编译器不会为它产生一个指针常量,sizeof返回的是整个数组占用的内存空间的大小而不是指针的长度,这也是数组名与指针的区别之一。
  • 声明一个数组,程序在运行时会为这个数组在栈上分配内存空间;声明一个指针,程序并不会分配对应的内存空间,这是指针与数组的又一个区别。
    声明如下:int a[5];int *b;
    *a是合法,将访问数组a的第一个元素;*b将访问内存中某个不确定的位置,可能造成程序终止。

3、数组引用方式

对于数组而言,除优先级不同,下标引用和间接引用是完全一致的,但间接引用在某些情况下效率更高。

4、数组参数

  • 数组名作为参数进行传递时,实际上仍然是传值调用。由于数组名被编译器视为一个常量指针,因此会传入这个指针的拷贝,表现为传址调用。
  • 对于函数中数组参数,在大部分情况下,声明数组参数与声明指针参数是相等的。
  • 如果形参中无长度声明,如int func(int a[]),这种情况下实际进行的是指针传递,声明数组参数与声明指针参数是相等的,所以形参数组可以匹配任意长度的实参数组,但是这种声明方式不包含长度信息,需要把长度信息作为显式参数进行传递。
  • 如果形参中有长度声明,如int func(int a[10]),编译期间编译器将检查实参类型,实参必须定义为长度是10的整型数组,指针类型的实参或者长度不为10的数组都无法通过编译。但这种情况,数组长度已经隐含在声明中。
  • 无论哪种声明形式,sizeof(形参数组)的结果都等于sizeof(指针)。

5、数组初始化

  • 数组初始化使用大括号,括号中每个值之间用逗号分隔;
  • 不完整初始化,即大括号中的值的数量少于数组元素的数量,未提供初始值的数组元素将被初始化为0;
  • 大括号中的值的数量不能超过数组元素的数量,否则无法通过编译;
  • 如果数组声明中未提供数组长度,编译器将把数组长度设置为刚好能容纳所有初始值的长度;
  • 对于代码块中定义的数组,在栈上分配内存,每次调用代码块时都将进行初始化,可以在数组前加上static使其变成静态数据;
  • 对于静态数组和全局数组,由链接器在PE文件中分配空间,通常保存在.data段,仅初始化一次;
  • 字符数组的初始化既可以使用前面的方法,可以用字符串进行初始化,如char str[]=”abcdef”;在这种上下文环境中,”abcdef”代表的是初始化列表而不是字符串常量,数组的长度为7。

6、多维数组

  • 多维数组本质上仍然是一维数组,其访问方式既可以用下标访问,也可以用间接访问;
  • 多维数组初始化时,只有第一维的长度可以根据初始化列表缺省提供,剩余几个维的长度必须显式提供;
  • 多维数组作为形参时,只有第一维的长度可以省略,剩余几个维的长度必须显式提供;这是因为编译器必须知道第2维及其以后各维的长度才能对各下标进行求值计算;
  • 指向数组的指针声明方式:int (*p)[10];
  • 指针数组的声明方式:int *p[10];下标引用的优先级高于间接引用的优先级。

第9章 字符串、字符和字节

字符串是非常重要的数据类型,在软件开发,绝大部分软件都要涉及对字符串的处理,并且字符串处理的代码通常占整个工程代码量的20%以上。对新手而言,字符串处理过程非常容易出现溢出的问题,或者存在溢出的漏洞。
C语言中并无显示的字符串数据类型,因为字符串以字符常量的形式存储于字符数组中,换言之,字符串就是字符数组。

1、字符串基础

字符串以’\0’结尾,因此字符串内部不能出现’\0’,’\0’本身不是字符串的一部分。
strlen函数返回一个字符串的长度,字符串长度不包括最后结尾的’\0’。这个返回类型是size_t类型,size_t被定义为无符号整数。
注意,下面两行代码的功能并不相同:
if(strlen(x)>=strlen(y))
if(strlen(x)-strlen(y)>=0)
第一行代码的执行结果与预期相同;但第二行代码中,由于两个无符号数相减的结果仍然是无符号数,无符号数是不会小于0的,所以if语句的结果永远为真。

2、长度不受限制的字符串函数

长度不受限制,是指这些函数通过寻找字符串结尾的NUL字符来判断字符串的长度,这类函数很容易造成溢出。
这类函数有strcpy,strcat,strcmp。
其中strcpy和strcat可能出现溢出,这两个函数返回第一个参数的拷贝,返回值常被忽略。strcmp是安全的。

3、长度受限的字符串函数

这类函数有strncpy,strncat,strncmp。

4、字符串查找函数

strchr strrchr strstr
strpbrk :查找字符集中任意字符在字符串中第一次出现的位置
strspn:查找字符串开始位置与指定字符集匹配的字符个数,到第一个不匹配的字符为止
strcspn :与strspn相反
strtok:字符串分割函数,会修改源字符串,不可重入

5、字符函数

toupper() tolower()
isdigit() isxdigit() iscntrl() isspace() islower() isupper() isalpha() isalnum() ispunct() isgraph() isprint()

6、内存操作函数

memcpy:源地址和目的地如果重合,结果是未定义的
memmove:功能与memcpy类似,源地址和目的地址可以重合,内部实现方式是分配一块临时空间,把源数据拷贝到临时空间,再从临时空间拷贝到目的地址
memset
memcmp

第10章 结构和联合

C语言提供了基本数据类型,又称标量。
C语言还提供了聚合数据类型,又称向量,需要程序猿自己定义。聚合数据类型可以同时存储超过一个的基本类型,包括数组和结构。

1、结构体基础

数组是相同类型的元素的集合,因此数组可以使用下标进行进行访问。
结构可以是不同类型的数据的集合,这些类型被称为成员,成员的类型不同,其长度也不同,因此结构体中成员不能用下标进行访问。
结构体的声明方式:stuct tag{member_list} vari_list;
结构的自引用:结构体不能包含一个类型为结构体自身的成员,编译器不允许这样做。如果允许这样,将造成一个无限嵌套。但结构体可以包含一个指向结构体类型的指针,因为指针的长度是固定的。
不完整声明:用于两个相互依赖的结构体的声明。

struct B;
struct A{
    struct B;
    int a;
};
struct B{
    struct A;
    char c;
};

2、结构体对齐

结构体的成员在分配时,受内存对齐的影响。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是某个数k的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。

1、内存对齐的原因

大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

2、对齐的含义

比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。
结构体对齐包括两个方面的含义:
1)结构体总长度;
2)结构体内各数据成员的内存对齐,即该数据成员相对结构体的起始位置;

3、结构体数据对齐规则

结构体数据对齐,是指结构体内的各个数据对齐。在结构体中的第一个成员的首地址等于整个结构体的变量的首地址,而后的成员的地址随着它声明的顺序和实际占用的字节数递增。为了总的结构体大小对齐,会在结构体中插入一些没有实际意思的字符来填充(padding)结构体。

在结构体中,成员数据对齐满足以下规则:
结构体中的第一个成员的首地址也即是结构体变量的首地址。
结构体中的每一个成员的首地址相对于结构体的首地址的偏移量(offset)是该成员数据类型大小的整数倍。
结构体的总大小是对齐模数(对齐模数等于#pragma pack(n)所指定的n与结构体中最大数据类型的成员大小的最小值)的整数倍。

3、参数传递

结构体变量可以使用传值调用的方式,但这样将消耗大量的栈空间,最好使用传址调用的方式。
两个结构体变量之间可以使用赋值符号,编译器将根据结构体的大小进行内存拷贝。其汇编语言伪代码如下:

mov ecx,sizeof(struct)
mov esi,struct_A
mov edi,struct_B
rep movs dword ptr[edi],dword ptr[esi]

在C语言中,两个结构体变量之间不能用关系运算符进行比较。

4、联合

联合的所有成员引用的是内存中相同的位置,只是按照程序猿的意图,解释为不同的数据类型。比如4个字节,可以解释为int类型,也可以解释为4个字节。

5、位域

位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。目前较少使用。

第11章 动态内存分配

如果声明一个数组,数组的大小在编译时就必须指定,运行时在栈上分配内存,比如,sub esp,0x10;
如果一块内存空间的长度在运行时才能确定,就需要动态内存分配。
C语言中动态内存分配的函数有malloc和calloc,二者的区别是calloc分配内存之后用0进行初始化,调用这两个函数之后应检查其返回值是否为NULL。分配后的内存需要调用free函数进行释放,否则会造成内存泄漏。当进程终止时,该进程未被释放的内存都将归还给内存池。即使这样,一个只申请内存不释放内存的程序仍然是危险的,存在内存耗尽的可能。
realloc函数更改已经配置好的内存空间,
如果将分配的内存减少,realloc仅仅是改变索引的信息。

如果是将分配的内存扩大,则有以下情况:
1)如果当前内存段后面有需要的内存空间,则直接扩展这段内存空间,realloc()将返回原指针。
2)如果当前内存段后面的空闲字节不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块位置。
3)如果申请失败,将返回NULL,此时,原来的指针仍然有效。
使用realloc函数时,要注意保存原始内存的地址,并调用free函数释放原来的内存,realloc并不会自动释放这块内存。

第12章 使用结构体和指针

使用结构体和指针可以创建强大而灵活的数据结构–链表,链表包括单链表、双向链表和循环链表。这一章主要讲有序单链表和有序双链表的插入操作。

第13章 高级指针

函数指针:函数名在使用时总是被编译器转换为函数指针。可以利用函数指针组成转移表。
命令行参数:main函数有两个命令行参数,argv和argc,argv表示参数个数,argc是字符串数组;
字符串常量:当字符串常量出现在表达式中时,它的值是个指向字符数组的指针常量,可以使用下标访问和解引用。”xyz”[0]和*”xyz”都表示字符’x’;”xyz”+1是个指针,指向字符’y’;

第14章 使用预处理器

编译C程序的第一个步骤是预处理器的处理,本质是在编译源代码之前进行一些文本替换。

1、预定义符号

ANSI C中的预定义符号包括:

预定义符号 含义
_FILE_ 正在进行编译的文件名
_LINE_ 文件当前的行号
_DATE_ 文件被编译时的日期
_TIME_ 文件被编译时的时间
_STDC_ 如果编译器支持ANSI,其值为1

不同的编译器还会定义一些其它的预定义符号,比如VC中定义的WINVER,DEBUG

2、宏定义

宏定义仅进行文本替换,替换之后可能存在运算符优先级与预期不一致的问题。
宏参数和宏定义可以包含其它的宏定义,但不能进行递归。
宏定义与函数相比,在运行可以减少堆栈开销,减少调用时间。宏定义与类型无关,不会进行类型检验。
宏定义具有副作用。
使用#undef可以移除一个宏定义。

3、头文件

头文件可以进行嵌套包含,标准要求至少支持8层嵌套。

第15章 输入/输出函数

perror():打印错误信息
exit():终止当前程序的执行

函数名称 功能
perror() 打印错误信息
exit() 终止当前程序的执行
tmpfile() 创建临时文件
tmpnam() 创建临时文件名
remove 删除文件
rename 对文件进行重命名

标准库中getchar()、putchar()、getc()、putc()的这四个函数都是宏定义,
#define getchar() getc(stdin)
#define getc(_stream) fgetc(_stream)
fgetc的返回值为int类型,所以getchar()的返回值也为int类型。

第16章 标准函数库

C语言提供了强大的函数库,包括:

  • 算术函数:abs(),labs(),div(),ldiv()
  • 随机函数:rand(),srand()
  • 字符串转换函数
  • 浮点类型函数
  • local函数
  • system函数

第17章 经典抽象数据类型

本章主要讲述经典数据类型,栈,队列和二叉树的内容。

第18章 运行时环境

你可能感兴趣的:(学习笔记,C和指针)