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语言数据类型

其中有些细节需要注意:
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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值