C语言深度解剖 学习笔记

C语言深度解剖 学习笔记标注

第一章 关键字

C语言总共共有32个关键字,除去常用的关键字,如下是晦涩的,需要谨记的:
1,sizeof 计算对象所占内存空间大小
2,union 声明联合数据类型
3,enum 声明枚举类型
4,const 声明只读变量
5,volatile 说明变量在程序执行中可被隐含的改变

首先介绍关键字之前,学习下定义和声明的含义区别:
定义:编译器创建一个对象,为这个对象分配一块内存并给它取名,该名字就是对象名或者变量名,名字在一定区域只能被定义一次,否则会报重复定义的错误
声明:告知编译器,名字已经匹配到一定的内存上,引用时,可以出现多次声明;
告知编译器,名字已经被预定,不能在其他地方出现被定义的情况;

其次介绍关键字:
register:寄存器,CPU内部的存储空间,速度非常快的进行数据存取。register定义时的类型必须是被CPU寄存器接受的类型,变量必须是单一的值,且不能比整型
长度大。且不能用“&”运算符获取变量名的地址

static:
1,修饰变量:
局部变量和全局变量,被static修饰时,都存在内存的静态区。
static的全局变量作用于本文件中定义的位置到文件的结束位置,就算extern修饰符,也不能被其他文件使用。
static的局部变量作用于本函数段,外部不能引用。
由于都存在内存的静态区,即使函数调用结束,变量也不会被销毁,下次进入函数时,还能被引用。
2,修饰函数
函数的作用域仅局限于本文件中

变量命名规则:
1, 直观拼读,望文知意
2, 命名简洁,多字达大意
3, 词组组成的标识符,首字母大写
4, 命名避免出现数字编号,除非出现管脚
5, 共享文件出现的变量命名,定义时需要明确范围限定符
6, 拒绝出现紧靠大小写区别的相似标识符
7, 函数名禁止用于它处
8, 所用宏定义,枚举常数,只读变量只能用大写,且用下划线_分隔符来分割
9, 可以采用通用的命名方式,如for循环语句中的i,n,j,k
一般来讲,n,m,i,j,k用于int型的变量;c ch表示字符类型变量;a表示数组,p表示指针,
10, 定义变量时,需要初始化
11, 精度的扩展需要从低精度到高精度

sizeof学习点:
sizeof在计算变量所占的内存大小时括号可以省略,计算类型(int, float)所占大小时,括号不能省略 一般都加上括号就行

signed unsigned: 编译器缺省下默认为signed 有符号类型
signed 有符号类型,最高位为0的正数,最高位为1的负数,unsigned 无符号类型

计算器系统中,数值都是用补码来表示,两个补码的数值相加时,最高位(符号位)有进位时,忽略进位,正数补码和源码一致,负数的补码,最高位1,其余位是负数绝对值源码按位取反并加1. -1补码是0xffff

使用if语句注意事项:
1, 先处理正常情况,再处理异常情况
2, 确保if else语句逻辑正常,没有弄反

switch case语句:
case后面只能是整型或者字符型常量或者常量表达式,字符型数组在内存中的存储

do,while,for语句:
1,多重循环中,内部循环放置最长的循环,以减少CPU的跨切循环层的次数
2,for循环的循环变量赋值采用半开半闭区间法,如for(int i = 0;i < 10; i++) 非for(int i =0; i<=9; i++)
3,for循环体内不能改变循环变量值,以防循环失控
4,循环体要短,代码清晰,一目了然
5,循环嵌套控制在3层以内

void关键字:
void修饰指针, void * 空指针类型指针,可以指向任何类型的数据。
赋值语句
报如下错误,D:\C\Project\HelloWorld.cpp [Error] cannot convert ‘int*’ to ‘float*’ in assignment,需要类型转换(float *)
void 没有此类报错,可以转换成任意类型的数据,但是任意类型不能转换成void
vod*编译器正常运行
1,如果函数没有返回值,那么应声明为 void 类型
2,如果函数无参数,那么应声明其参数为 void

void* 指针
按照ANSI规则,void*指针不能进行算法操作,但是按照GNU下是可以进行算法操作的
如果函数的参数可以是任意类型指针,那么应声明其参数为 void *

return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时
被自动销毁
const 可读的变量,编译时不能被使用,编译器在编译时不知道其存储的内容,定义只读变量时,不可改变。
const 修饰的只读变量必须在定义的同时初始化

const节省空间,避免不必要的内存分配,同时提高效率
编译器通常不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使
得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

const只是给出了对应的内存地址,象#define
一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝。#define 宏是在预编译阶段进行替换,而 const 修饰的只读变量是在编译的时候确定其值。#define 宏没有类型,而 const 修饰的只读变量具有特定的类型

const修饰的指针,编译时忽略数据类型,如下的int类型忽略
const int *p; // p 可变,p 指向的对象不可变
int const *p; // p 可变,p 指向的对象不可变
int *const p; // p 不可变,p 指向的对象可变
const int *const p; //指针 p 和 p 指向的对象都不可变

修饰函数返回值和参数,都是不可改变的属性

volatile 关键字:用它修饰的变量表示可以被某些编译器
未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

struct 关键字:它将一些相关联的数据打包成一个整体,方便使用
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结
构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

enum 关键字:自定义的一种数据数据类型名,实际上enum_type_name
类型是对一个变量取值范围的限定,而花括号内是它的取值范围,即 enum_type_name 类型的变量 enum_variable_name 只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。

enum和#define的区别:
1),#define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3),枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个

typedef:typedef 的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型

第二章 符号
注释的基本要求:
1, 注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
2, 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。
不再有用的注释要及时删除。
3, 注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太
多了会让人眼花缭乱。
4, 一目了然的语句不加注释。
5, 对于全局数据(全局变量、常量定义等)必须要加注释。
6, 注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写
7, 注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但
不可放在下方。同一结构中不同域的注释要对齐。
8, 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于
阅读。
9, 注释的缩进要与代码的缩进一致。
10, 注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”。
说明怎么做的注释一般停留在编程语言的层次,而不是为了说明问题。尽力阐述“怎么做”
的注释一般没有告诉我们操作的意图,而指明“怎么做”的注释通常是冗余的。
11, 数值的单位一定要注释。
注释应该说明某数值的单位到底是什么意思。比如:关于长度的必须说明单位是毫米,
米,还是千米等;关于时间的必须说明单位是时,分,秒,还是毫秒等
12, 对变量的范围给出注释。
13, 对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚
编号).
14, 对于函数的入口出口数据给出注释

运算符的优先级:


Testapk
PS:乘除法的优先级比加减法高就行了,别的地方一律加上括号;


第三章 预处理

1, 宏定义

宏定义常量和常量表达式
#define PI 3.141592654
从本行宏定义开始,以后的代码就就都认识这个宏了;也可以把任何东西定义成宏。
为了安全,建议在定义一些宏常数的时候用 const代替,编译器会给 const 修饰的只读变量做类型校验,减少错误的可能。但一定要注意 const修饰的不是常量而是 readonly 的变量,const 修饰的只读变量不能用来作为定义数组的维数,也不能放在 case 关键字后面。

宏除了定义常数外,还可以定义字符串常量。特别是文件路径的定义
#define ENG_PATH_2 “E:\English\listen_to_this\listen_to_this_3”
路径太长时,注意反斜杠符表示的路径分隔符和持续符的 不同之处,反斜杠符作为持续符时,后面不能跟空格和任何字符。强调一点,有的系统里规定路径的要用双反斜杠“\”
PS:define不能定义注释符,由于注释符先于预处理define宏定义编译。

宏定义表达式时,注意括号的添加。如下,宏定义表达式
#define SUM(x) (x)+(x),当x=53时,代码写成SUM(x)SUM(x) = (53) + (53)(53)+(53)=255,如果预期想表示1515=225时,就会出错。所以表达式定义为
#define SUM(x) ((x)+(x))

2,条件编译
条件编译是按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。
#ifdef 标识符
程序段 1
#else
程序段 2
#endif

标识符被#define定义时,编译程序段1,否则编译程序段2,如果程序段2为空时,#else 程序段2取消。

#ifndef 标识符
程序段 1
#else
程序段 2
#endif
标识符未被#define定义时,编译程序段1,否则编译程序段2.

#if 常量表达式
程序段 1
#else
程序段 2
#endif
表达式为非0时,编译程序段1,否则编译程序段2。#elif 命令意义与 else if 相同,它形成一个 if else-if 阶梯状语句,可进行多种编译选择

3.文件包含
预处理时文件包含是一个重要的功能,导入需要引用的其他文件的函数或者变量,格式书写如下两种:
#include
#include “filename”
文件名用<>包含,表示预处理到系统规定的路径获取这个文件;用“”包含表示在当前目录下找寻该文件。#include是将已存在文件的内容嵌入到当前文件中,支持相对路径,.表示当前目录,…表示上层目录。

4,#error 预处理
编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。其语法格式为:#error error-message

5,#line 预处理
#line number[“filename”]
改变当前的行号和文件名

6,#pragma 预处理
设定编译器的状态或者是指示编译器完成一些特定的动作。编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。其格式一般为:#pragma para para 为参数

#pragma message
message 参数 它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:#pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来,如下:
#ifdef _X86
#Pragma message(“_X86 macro activated!”)
#endif
希望判断自己有没有在源代码的什么地方定义了_X86 这个宏

#pragma code_seg
#pragma code_seg( [“section-name”[,“section-class”] ] )
设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到

#pragma once 保证头文件被编译一次。#pragma hdrstop 预编译头文件到此为止,后面的头文件不进行预编译。#pragma resource #pragma resource ".dfm"表示把.dfm 文件中的资源加入工程。*.dfm 中包括窗体外观的定义。

#pragma comment
常用的 lib 关键字,可以帮我们连入一个库文件。 比如:#pragma comment(lib, “user32.lib”)该指令用来将 user32.lib 库文件加入到本工程中。
linker:将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的或
者在开发环境中设置的链接选项,你可以指定/include 选项来强制包含某个对象,例如:
#pragma comment(linker, “/include:__mySymbol”)

#pragma pack
内存对齐说法:
使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。
首先,每个成员分别按自己的方式对齐,并能最小化长度。
其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂
类型时,可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保
证每一项都边界对齐。

但是不论类型是什么,对齐的边界一定是 1,2,4,8,16,32,64…中的一个。

#运算符
在字符串中包含宏参数,那我们就可以使用“#”,它可以把语言符号转化为字符串。
#define SQR(x) printf(“The square of “#x” is %d.\n”, ((x)*(x))); SQR(8)时,显示的是The square of 8 is 64

##预算符
用于宏函数的替换部分,#define XNAME(n) x ## n
XNAME(8)则会被展开成这样:x8

第四章 指针和数组

1,指针
int *p;所示定义,命名p,p存储某个内存的地址,而且从该地址起始的连续四个字节存储int型数据。在32位系统中,size§不管 *前面的是什么类型都是32Bit 四个字节
指针定义图示
p 称为指针变量,p 里存储的内存地址处的内存称为 p 所指向的内存。指针变量 p 里存储的任何数据都将被当作地址来处理。

int p = NULL 和p = NULL 区别:
int p = NULL 定义一个指针变量 p,其指向的内存里面保存的是 int 类型的数据;在定义变量 p 的同时把 p 的值设置为0x00000000,而不是把p 的值设置为 0x00000000。这个过程叫做初始化,是在编译的时候进行的。定义的是p里存储的内存地址为null
*p = NULL 给 p指向的内存赋值为 NULL;但是由于 p 指向的内存可能是非法的,所以调试的时候编译器可能会报告一个内存访问错误。变量 p 保存的有可能是一个非法的地址

将数值存储到指定的内存地址,*(int )0x12ff7c = 0x100;
上式表示把0x12ff7c作为指针地址,在该地址上存储0x100数据。将地址 0x12ff7c 强制转换,告诉编译器这个地址上将存储一个 int 类型的数据;然后通过钥匙“
”向这块内存写入一个数据。

2, 数组
数组形象示意图
int a[5]; 定义一个数组 a 时,编译器根据指定的元素个数和元素的类型分配确定
大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为 a。名字 a 一旦与这块内存匹配就不能被改变,a[0],a[1]等为a的元素,但并非元素的名字。数组的每一个元素都是没有名字的.
sizeof(a)的值为 sizeof(int)*5,32 位系统下为 20。
sizeof(a[0])的值为 sizeof(int),32 位系统下为 4。

a[5]元素并不存在,但sizeof(a[5])的值在 32 位系统下为 4。由于函数求值是在运行的时候,而关键字 sizeof 求值是在编译的时.
sizeof(&a[0])的值在 32 位系下为 4,这很好理解。取元素 a[0]的首地址。
sizeof(&a)的值在 32 位系统下也为 4,这也很好理解。取数组 a 的首地址

a[0]是一个元素,a 是整个数组,虽然&a[0]和&a的值一样,但其意义不一样。前者&a[0]是首元素的首地址,&a是数组的首地址。
int a[5] = {1,2,3,4,5};
printf("%x\n",&a);
printf("%x\n",&a[0]);
printf("%x\n",a+1);
printf("%x\n",&a+1);
程序运行结果
&a,数组首地址,&a[0],首元素首地址,a+1 第二元素首地址,&a+1 数组首地址+数组长度(4*5=20)

x=y;
数组名出现=号右边的右值,在这个上下文环境中,编译器认为 y 的含义是 y 所代表的地址里面的内容。这个内容是什么,只有到运行时才知道,代表的是数组首元素的首地址
数组名出现=号左边的左值,在这个上下文环境中,编译器认为 x 的含义是 x 所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址。
数组名作为右值是代表的是数组首元素的首地址,这仅仅是代表,并没有一个地方来存储这个地址。数组名不能作为左值,只能通过a[n] = 来操作。

指针与数组:
指针就是指针,指针变量在 32 位系统下,永远占 4 个 byte。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型
和个数。数组可以存任何类型的数据,不能存放函数

定义 char p = “abcdef” 访问指针
1,以指针的形式访问
定义一个指针变量p,32位系统中,大小为4个字节byte,存储的是地址,地址下的内容是7个字节byte,为abcdef。
(p+4),先取出p里存储的地址值,然后进行4个字符的偏移量,然后取出该地址的值。
2,以数组的形式访问
p[4]的形式也可以访问,取出e字符。编译器把以下标的形式的操作解析为以指针的形式的操作,以下标的形式访问在本质上与以指针的形式访问没有区别。

定义char a[] = “123456”; 访问数组
1, 以指针的形式访问,*(a+4)。a 这时候代表的是数组首元素的首地址,然后加上 4 个字符的偏移量,取出地址上的值
2, 以数组的形式访问,a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操

PS:个偏移量的单位是元素的个数而不是 byte 数

a 和&a 的区别:
int a[5]={1,2,3,4,5};int *ptr=(int *)(&a+1);
对指针进行加 1 操作,得到的是下一个元素的地址,一个类型为 T 的指针的移动,以 sizeof(T) 为移动单位.取数组 a 的首地址,该地址的值加上 sizeof(a) 的值,即下一个数组的首地址,显然当前指针已经越过了数组的界限。(int *)(&a+1): 则是把上一步计算出来的地址,强制转换为 int * 类型,赋值给 ptr。
*(a+1): a,&a 的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是 a[0]的
首地址,&a 是数组的首地址,a+1 是数组下一元素的首地址,即 a[1]的首地址,&a+1 是下一个数组的首地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值