0、数据类型可以理解为固定内存大小的别名 数据类型是初期建立便于利用的创建变量的模子
1、变量是只是一段内存的别名,通过别名来使用内存
Tips
(
计算机硬件的存储本身是包含是浮点数,和定点数,然后读取这片内存的时候,按照原本已经制定好的规则(比如浮点数,或定点数)读取
)
2、c++的引用也是给这一段内存起的另一个别名,
3、register 变量的大小必须小于四个字节,如果变量申请的空间过大就会导致最终出错,数据溢出。(为什么不允许register修饰全局变量,全局变量存在静态区,在运行是一直存在的,如果可以放在全局变量里,一旦数据一多,register 会崩溃的,而导致Cpu 蹦溃)
4、自定义变量 typedef 的用法
变量是内存的别名,自定义的变量是对存在的类型进行重命名,不是创建变量
Typedef int INT32;
第二节
属性关键字 auto ,static register
Auto c语言中的变量可以有自己的属性
在定义的时候可以加上“属性”关键字
属性关键字指明变量有特殊的意义
auto
1、auto 是c语言中所有的局部变量的默认属性,自动创建,函数完毕自动释放的
2、在栈里分配的空间,
Static
1、static 关键字指明变量的静态属性,
2、全局变量的默认为静态属性,但不加static可以外调,但是加了static 就不可以外调,
3、函数的使用,用static修饰的函数限定只能在本文件内使用,所有的函数都是全局函数
4、全局变量的默认是静态属性,加上static 表示只能在本文件内使用
5、在函数里修饰的话,只初始化一次。只能在函数内部使用,但不会被销毁,他是在全局变量数据区的,只是有作用域(Tips 假使文件对 Extern )
6、作为全局变量作用域从定义处开始直到文件结尾,定义处前面要引用的话必须加 extern
7、extern 如果访问了其他文件的static变量,会出错,但是如果初始化了的话,就生成该变量在这个文件下的用 extern int i= 34; 不允许修改的,只可以在其他的地方进行修改。默认为exten,任何的全局变量都是默认了Extern的,只是显示的调用
// 参考: 1、初始的用途是定义的局部静态属性,后来又定义了一个限制在本文件内使用
Register
1、register 关键字指明变量存储在寄存器中
2、Registere只是请求放在寄存器中,不一定能申请的到
3、ERROR register 变量必须是cpu可接受的值,
4、不能用&运算符获取register变量的地址,对实时性要求特别高的话就需要用寄存器变量
//全局变量 设为auto 或者register变量,全局变量的默认是全局静态区分配的,
而 用这个的话会产生警告,甚至有的编译器直接报错
第三节
if语句中零值比较的注意点
1、bool型变量应该直接出现于条件中,不要进行比较
2、普通变量和0值比较时,0值应该出现在比较符号左边
3、float型变量不能直接进行0值比较,需要定义精度
4、布尔值最好出现在左边。
C语言中不存在布尔变量,通常情况下都是自己编译器自己定义的,而编译器的不同,true和false代表的值可能不同,所以判断这个会容易出错,最好使用替代的词,如 true,或false
,或者直接加叹号处理
‚注意浮点型是连续的值,而计算机表示的二进制描述的一个非连续数,所以这个表现的很有误差是可以理解的
ƒ的处理用 #define e 0.00000001
Float fa 比较方法 if(fa < = fa+e && fa > = fa -e)
switch语句对应单个条件多个分值的情形
1、switch处理的是一个条件,多个分支的情况只能处理整形和char型的
2、每个case语句分支必须要有break,否则会导致分支重叠
3、default语句有必要加上,以处理特殊情况
4、case语句中的值只能是整型或字符型
5、case语句排列顺序分析
6、按字母或数字顺序排列各条语句
7、正常情况放在前面,异常情况放在后面
8、default语句只用于处理真正的默认情况
9、if语句实用于需要“按片”进行判断的情形中
10、switch语句实用于需要对各个离散值进行分别判断的情形中
11、if语句可以安全从功能上代替switch语句,但switch语句无法代替if语句
12、switch语句对于多分支判断的情形更加简洁
循环: while ,do while(), for
便捷性,for循环不易出错,do { } while要有检错语句
一 while(n && ret += n--)
一般函数设计原则
1、分配资源语句 2、执行控制语句 (判断是否可继续执行) 3继续执行
int func(int n)
{
int i = 0;
int ret = 0;
int* p = (int*)malloc(sizeof(int) * n);
do
{
if( NULL == p ) break;
if( n < 0 ) break;
for(i=0; i<n; i++)
{
p[i] = i;
printf("%d\n", p[i]);
}
ret = 1;
}while(0);
free(p);
return ret;
}
break和continue的区别
break表示终止循环的执行
continue表示终止本次循环体,进入下次循环执行
switch能否用continue关键字?为什么?
不能,因为break,天生用于终止块操作的。Loop and switch 其他的都是顺序是执行,跳出的无意义
第四节
Goto
1、 高手潜规则 一般很少使用Goto关键词
2、 项目经验,Goto用的次数越多,则项目越差
3、 所有人员都认为Goto 不好用,决定禁止使用
¥ 由于程序中直接跳转带来了结构化程序的不好控制,跳过了堆程序的内存分配,后期使用的时候会使程序奔溃,它破坏了程序的至上而下的规则。
Void 基础类型
1、修饰函数参数,不接受任何参数int add( );
2、修饰函数返回值,表示不返回值 void add();
(Tips: Void表示几个内存单位呢,这是c语言中的灰色单位,他代表无变量或无值,由于c语言标准没有申明,所以不同的厂商对这个的决策也不一样,有的用占一个字节,有的却没有。C++中明令禁止,不允许这种操作,所以不允许void oid占的内存大小是厂商自定义的。
3、不允许定义 Void i;
4.、任何void * 作为作为左值可以接受任意的类型,而作为右值类型必须进行强制转化,很多可以通过,是因为要求不严格,为了可移植性,强制转换是必须要写的。
(C语言规定只有相同类型的指针才可以相互赋值)
函数设计方法
函数的参数
‚函数的算法设计
ƒ函数的返回值
Void的使用 void * memset(void* p ,char v,int size) 函数作用把所有的数据都清零,可以接受任意类型的地址,并且可以把所有都可以改变。
Extern
1、申明外部定义的文件或者变量和函数
2、作为c的标准指示字,C++编译器和一些变种C编译器默认会按“自己”的方式编译函数和变量,通过extern关键可以命令编译器“以标准C方式进行编译”。
extern "C"
{
int add(int a, int b)
{
return a + b;
}
}
Sizeof()
1、sizeof是编译器的内置指示符,不是函数
2、sizeof用于“计算”相应实体所占的内存大小
3、sizeof的值在编译期就已经确定,意味着运行期的时间点的设置是无效的,例如: sizeof(i++)起不到时间点的作用
4、int a ,,double b sizeof( a+b ) 选择其中占用内存最大的一个值
Sizeof不是函数的各种用法
sizeof (int) ‚ sizeof c ƒ sizeof (c)
问题 sizeof int 为什么不可以求出其中的整形变量的内存的大小?
答 原因:在编译器会认为sizeof 修饰int, 而int类型的只可以 unsigned和signed,
sizeof修饰 int 是不允许通过的,故而报错,所以禁止使用 sizeof TypeName
5、数组做sizeof的参数不退化,传递给strlen就退化为指针了。
6、sizeof操作符的结果类型是size_t,它在头文件中typedef为unsignedint类型。
7、该类型保证能容纳实现所建立的最大对象的字节大小。
8、2.sizeof是算符,strlen是函数。
9、3.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。
10、sizeof还可以用函数做参数,比如:
| Short f(); printf("%d\n",sizeof(f()) ); |
输出的结果是sizeof(short),即2。即返回值的类型大小
数组做sizeof的参数不退化,传递给strlen就退化为指针了。
11、大部分编译程序在编译的时候就把sizeof计算过了是类型或是变量的长度这就是sizeof(x)可以用来定义数组维数的原因
12、 13、 14、 | Char str[20]="0123456789"; Int a=strlen(str);//a=10; Int b=sizeof(str);//而b=20; |
15、strlen的结果要在运行的时候才能计算出来,是用来计算字符串的长度,不是类型占内存的大小。
15、sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。这是因为sizeof是个操作符不是个函数。
16、8.当适用于一个结构类型时或变量, sizeof 返回实际的大小,当适用于静态的空间数组, sizeof 归还全部数组的尺寸。
17、sizeof 操作符不能返回被动态分派的数组或外部数组的尺寸
18、数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,
第五节
Const的用法
1、在C语言中const修饰的变量不能通过变量名进行改写操作,其本质还是变量,通过指针对其值进行修改是允许的
2、const修饰的变量会在内存占用空间
3、本质上const只对编译器有用,在运行时无用(即运行时可以改变他的值)
4、对其改写操作例如: const int a = 89; int *p = &a; *p = 67; 改写成功
5、现代的编译技术中,在初期作为常量存入符号表,然后后期,要修改的时候临时分配空间并给他改变他的值,然而下次使用的时候第一次使用的时候都是从符号表,
如果程序中const修饰的整个文件都是作为右值而不动地址,就会只是单纯的替换,如果程序中出现对该变量取地址操作时时,就会到一直从内存里面取值读出。
‚作为左值时直接报错(Tips 在c++ 中 在初期作为常量存入符号表,然后后期,要修改的时候临时分配空间并给他改变他的值,然而下次使用的时候第一次使用的时候都是从符号表读取,即使内存改变了他的值,)
6 、 在C语言中const修饰的数组是只读的
‚ const修饰的数组空间不可被改变 相当于指针和值都加了const, visual c++ 6.o 可通过,而c++中就绝对不允许这样操作的
7区别修饰的const
const int* p; //p可变,p指向的内容不可变
int const* p; //p可变,p指向的内容不可变
int* const p; //p不可变,p指向的内容可变
const int* const p; //p和p指向的内容都不可变
(Tips
Const Typename *p ; // p 可变,p指向的内容不可变
Typename * Const p; // p不可变,p指向的内容可变
)
方法 左数在右指,* 号出现的位置,数据区就数据不可改变,指针区指针不可改变
7、 const修饰函数参数表示在函数体内不希望改变参数的值
const修饰函数返回值表示返回值不可改变,多用于返回指针的情形
修饰函数指针表示,返回的指针的指向是不准改变的,
(注意可以通过其他别名或指针操作这块内存,修饰符修饰的师某一个变量名的权限, 而没有限制的变量当然可以操作的)
Volatile的用法
1、volatile可理解为“编译器警告指示字”
2、volatile用于告诉编译器必须每次去内存中取变量值
3、volatile主要修饰可能被多个线程访问的变量
4、volatile也可以修饰可能被未知因数更改的变量
编译器选择的优化,提高效率,某些变量值在文件中一直到那个做左值,编译器会直接从符号表中读取,而,当前线程睡眠,将其他线程的线程开启,(如硬件中断),如果线程改变其中得值,数据从符号表中读取,就会引起错误。(不可预期的错误 error of expectation)
思考题 volatile 和const是否可以修饰同一个变量呢? 又有什么意以?
答可以共存 ,对于const来说,应该理解为只读的意思,如果一个变量被定义为const了,那么它告诉使用者这个变量是只读的,不要试图在当前的模块中改变它。 而对于volatile来说,它是告诉编译器,这个被声明的变量可能在本程序之外被修改,告诉编译器每次在使用这个变量的时候都需要重新加载,不能优化。 只读表示编译器不允许代码修改变量。但并不表示这个变量在其它地方不能够被修改。
第六节
Struct 结构体变量
1、空结构体在计算机中占有多少内存?
c语言标准没有准确的指出,所以不同的市场的厂商的理解有一个字节也有为空的,一个字节的认为,这样子用空结构体创建的变量的内存地址相同,所以只能找最小的单位一字节,这样子创建的两个结构体变量就不会相同了。
(
Tips: Void表示几个内存单位,这也是c语言中的灰色单位,他代表无变量或无值,由于c语言标准没有申明,所以有的厂商认为是零,有的直接报错,
)
2、柔性数组即数组大小待定的数组
C语言中结构体的最后一个元素可以是大小未知的数组
C语言中可以由结构体产生柔性数组
Struct _soft \\结构体名
{
Int length;
int arr[];
}softarr;
柔性数组占有的内存大小是结构体的长度的大小的成员所占的大小,而无大小的数组是一个占位符,不占有内存,可直接使用
‚如何申请空间
Softarr * pst = (softarr * )malloc(sizeof(softarr) + len* sizeof(Typename));
ƒ 使用 pst->arr[i] ......, (数据全部导入后,该数组的大小还是成员变量的大小,而数组变量
3、struct的成员变量的空间都是独立的,自己申请的
Union
truct中的每个域在内存中都独立分配空间
union只分配最大域的空间,所有域共享这个空
实用注意事项:
大端模式 高地址放低位 →→→→
小端模式 低地址放低位 ←←←←
Enum
1、enum是一种自定义类型,默认首元素为0,以后类推的不断增加,
2、enum默认常量在前一个值的基础上依次加1
3、enum类型的变量只能取定义时的离散值(必须都是int 类型的 )
4、Enum 是真正的常量,不用分配空间,故无法取地址,且只能初始化一次,在定义的时候赋值,其他的时候都不允许,
6、#define宏常量只是简单的进行值替换,枚举常量是真正意义上的常量
7、#define宏常量无法被调试,枚举常量可以
8、#define宏常量无类型信息,枚举常量是一种特定类型的常量
9、enum与const变量的区别,enum是真正意义上的常量,而const只是只读属性,可以间接改变其值,而且具有分配的内存,可以获取地址。
Typedef 的用法
Typedef 的意义是什么?
答 : Tyedef 是对已经存在的类型进行重新命名,而不是定义新的数据类型
1、typedef用于给一个已经存在的数据类型重命名
2、typedef并没有产生新的类型
3、typedef重定义的类型不能进行unsigned和signed扩展
4、typedef是给已有类型取别名
5、#define为简单的字符串替换,无别名的概念
第二章
一 注释符号的使用技巧 :
1、注释方法
/*
*///
2、注释应该准确易懂,防止二义性,错误的注释有害而无利
3、注释是对代码的提示,避免臃肿和喧宾夺主
4、一目了然的代码避免加注释
5、不要用缩写来注释代码,这样可能会产生误解
6、注释用于阐述原因而不是用于描述程序的运行过程
7、注释符号相当于空格,而不是其他的,所以要注意使用
8、注释符不能出现在双引号之间,否则将会视为字符串
9、编译器会在编译过程删除注释,但不是简单的删除而是用空格代替
10、编译器认为双引号括起来内容都是字符串,双斜杠也不例外
“/*……*/”型注释不能被嵌套
11、// statements
二 接续符和转义符
接续符
1、编译器会将反斜杠剔除,跟在反斜杠后面的字符自动解到前一行
2、在接续单词时,反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格
3、接续符适合在定义宏代码块时使用比如定义像函数之类的
例如:#define swap(a,b) { \
int temp = a; \
tem p= a; \
a = b; \
b = temp ; \ }
4、接续符的是指反斜杠在一行中的最后一个字符,告诉编译器下一行的内容是本行的,并且直接连接。
转义符
1、C语言中的转义符(\)主要用于表示无回显字符,也可用于表示常规字符
2、C语言中的反斜杠(\)同时具有接续符和转义符的作用
3、当反斜杠作为接续符使用时可直接出现在程序中
4、当反斜杠作为转义符使用时需出现在字符或字符串中
三 单引号和双引号
1、本质上单引号括起来的一个字符代表整数
2、双引号括起来的字符代表一个指针
3、C编译器接受字符和字符串的比较,可意义是错误的
4、C编译器允许字符串对字符变量赋值,其意义是可笑的
5、char* p1 = (char *)1 ;//p1指向内存地址为一的地址,低地址一般要留给系统用的,不可能存在字符串给它用
6、char* p2 = (char *)'1';//'1'这是一个aslla 码,代表一个整数,,其实这句话的意思让 p2指向内存的地址为‘1’的地方
7、char* p3 = "1"; // 指向全局变量的静态区,printf(p3);
8、printf(‘\n’);// printf(); 接受一个字符串指针进行读取(遇见\0 结束) 而‘\n’地址是给系统用的,它不可能可以访问的,所以出现段错误。
9、C语言中的单引号用来表示字符常量 ‘a’+1 表示 ‘b’
10、C语言中的双引号用来表示字符串常量 “a”+1 字符首地址右移一位
11、字符
四 逻辑运算符使用分析
1、程序中的短路
短路规则:
||从左向右开始计算,当遇到为真的条件时停止计算,整个表达式为真;所有条件为假时表达式才为假。
&&从左向右开始计算,当遇到为假的条件时停止计算,整个表达式为假;所有条件为真 时表达式才为真
2、三目运算法
a < b ? a : b
返回的是变量的值,而不是变量,而 c++在这里进行了改进,返回的是这个的引用。
‚ *(a < b ? &a : &b) 改进方式
五 位运算
C语言号称高级语言为什么支持位运算?
在嵌入式开发中,会经常与外部链接,串口和并口很需要位运算的
1、按位与 2 & 3 010 & 011 = 010
2、按位或 2 | 3 010 | 011 = 011
3、按位异或2 ^ 3 010^011 = 011
有结合律和交换律
4、左移和右移注意点
左移运算符<<将运算数的二进制位左移
规则:高位丢弃,低位补0
5、右移运算符>>把运算数的二进制位右移
规则:高位补符号位,地位丢弃
6、防错准则:
避免位运算符,逻辑运算符和数学运算符同时出现在一个表达式中
当位运算符,逻辑运算符和数学运算符需要同时参与运算时,尽量使用括号()来表达计算次
序
位移的大小不能大于输的长度且不能为负的
7、Tips
左移n位相当于乘以2的n次方,但效率比数学运算符高
右移n位相当于除以2的n次方,但效率比数学运算符高
8、交换变量的三种方式
#define SWAP1(a,b) \
{ \
int temp = a; \
a = b; \
b = temp; \
}
‚#define SWAP2(a,b) \
{ \
a = a + b; \
b = a - b; \
a = a - b; \
}//比第一种少用了一个变量, 当a 和 b 很大的时候会溢出
ƒ#define SWAP3(a,b) \
{ \
a = a ^ b; \
b = a ^ b; \
a = a ^ b; \
}//效率最高,但只适合整形 (a ^ a) ^b = b
试题: 有一个数列,其中的自然数都是以偶数的形式出现,只有一个自然数出现的次数为奇数次。编写程序找出这个自然数。
方法一: 将数列排序,排序后去读取,一旦奇数个就跳出
方法二: 用空间换时间,先找到最大的数n,然后申请n个空间,,把数组清零
然后用循环for (...) b[a[i]]++ ; 最后遍历数组的大小,一但是奇数就返回。
方法三: 用按位异或不断的消除,最后只剩下一个就是要的答案
#include<stdio.h>
Int main()
{
Int a = {1,1,1,2,3,4,5,6,5,6,4,3,2};
Int i = 0;
Int find = 0;
For (i = 0; i< (sizeof(a)/sizeof(int )), i++)
Find = find^a[i];
Printf(“%d\n”,find;
Return 0;
}
自增自减
I = 3;
++i ++i++i
这是一个c语言的灰色地带,不同的编译器有不同的值,在visual c++ 5.0 中
I = 3;
(++i, ++i,++i) 逗号表达式,从左到右开始做
贪心法 -- ++, --表达式的阅读技巧
1、编译器处理的每个符号应该尽可能多的包含字符
2、编译器以从左向右的顺序一个一个尽可能多的读入字符
3、当即将读入的字符不可能和已读入的字符组成合法符号为止
4、求值或报错,不能再读的时候,
例如 第一次读取 ++i + + 读到这的时候,发现已经不能在读到表达式使得表达式有意义,数值 ++, 没有办法,编译器报错 ,增加空格之后,贪心法的解析时,空格即为终止符,因此空格在容易混的地方能够更好的解析。
(Tips
在c语言中 ++ i 与 i++ 在表达式中只相当与右值。
在c++中,允许++ i 当做左值;
)
运算顺序:
C语言的隐式转换
C语言隐式类型转换
算术运算式中,低类型转换为高类型
赋值表达式中,表达式的值转换为左边变量的类型
函数调用时,实参转换为形参的类型
函数返回值,return表达式转换为返回值类型
预编译
1、•处理所有的注释,以空格代替
2、•将所有的#define删除,并且展开所有的宏定义
3、•处理条件编译指令#if, #ifdef, #elif, #else, #endif
4、•处理#include,展开被包含的文件
5、•保留编译器需要使用的#pragma指令
编译
1、•对预处理文件进行一系列词法分析,语法分析和语义分析
2、•词法分析主要分析关键字,标示符,立即数等是否合法
3、•语法分析主要分析表达式是否遵循语法规则
4、•语义分析在语法分析的基础上进一步分析表达式是否合法
5、•分析结束后进行代码优化生成相应的汇编代码文件
汇编
1、•汇编器将汇编代码转变为机器可以执行的指令
2、•每个汇编语句几乎都对应一条机器指令
链接器的作用
1、连接器的主要作用是把各个模块之间相互引用的部分
2、处理好,使得各个模块之间能够正确的衔接。
编译器将编译工作主要分为预处理,编译和汇编三部
1、 连接器的工作是把各个独立的模块链接为可执行程序
2、 静态链接(object文件和libc.a文件放在一起。)在编译期完成,动态链接(公用的数据建立一个区,想要用的话就去调(lib.so) 本质是调用外部函数,而静态链接是把所有的函数和数据放在同一个文件里的。))在运行期完成
(Tips 静态链接占的空间较大,但运行效率高,动态连接占用空间少,但运行效率低)
#define
1、#define表达式给有函数调用的假象,却不是函数
2、#define表达式可以比函数更强大
3、#define表达式比函数更容易出错
4、最好不要定义时间点的量,只选择单纯的变量,如 i++ ,
5、#define PT 允许这样定义,但没有仍和意义,将 PT 的内容替换为空,或者说,是直接删去
6、define可以替代任何变量,这个操作只是预编译中,替换而已
7、define的功能比函数强大,但是漏洞比函数大的多。
8、#define DIM(Array) (sizeof(Arryay)/sizeof(*Array))
9 、函数天生的弱点是数组会再调用期间退化成指针,引起数组大小的丢失,所以必须加上length,在本文件内我们会有意识的使用,在不同文件中访问会容易出错。
10、可以扩展C语言的用法,而函数却不能。
11、宏表达式在预编译期被处理,编译器不知道宏表达式的存在
12、宏表达式用“实参”完全替代形参,不进行任何运算
13、宏表达式没有任何的“调用”开销
14、宏表达式中不能出现递归定义
定义的内置宏,用于实现发现那个行出错的消息。
__LINE__ __FILE__ __DATE__
__STDC__ __TIME__
标准的日志宏:
#define LOG do \
{ \
time _t t; \
struct tm* ti; \
time(&t); \
ti = localetime(&t); \
printf("%s [%s:%d] %s \n", asctime(ti), __FILE__, __LINE__,s); \
}while(0);
条件编译
1、条件编译的行为类似于C语言中的if…else
2、条件编译是预编译指示命令,用于控制是否编译某段代码
#include的困惑
1、#include的本质是将已经存在的文件内容嵌入到当前文件中
2、#include的间接包含同样会产生嵌入文件内容的动作
如果间接的包含相同的头文件怎么办?会不会出错?
会出错,但是处理的方式是 ifndef _headname_ H_
条件编译的意义:
1、条件编译使得我们可以按不同的条件编译不同的代码段,
因而可以产生不同的目标代码
2、#if…#else…#endif被预编译器处理;而if…else语句被
编译器处理,必然被编译进目标代码
3、 实际工程中条件编译主要用于一下两种情况:
Ø 不同的生产线共用一份代码
Ø 区分编译产品的调试版和发布版
Ø 通过编译器命令行能够定义预处理器使用的宏
Ø 条件编译可以避免重复包含头同一个头文件
Ø 条件编译是在工程开发中可以区别不同产品线的代码
Ø 条件编译可以定义产品的发布版和调试版
如何实现在命令行实现宏定义
EXAMPLE : GCC -DEBUG -DHIGH //同时定义多个宏
GCC -DEBUG = \”CZCZXCZX\”
GCC - D MAX= 2 //定义 #define MAX 2
GCC -DCOMMAND =\”SDCSCS\” 定义字符串
#error用于生成一个编译错误消息,并停止编译
用法
#error message
注:message不需要用双引号包围
#error编译指示字用于自定义程序员特有的编译错误消息
类似的,
#warning用于生成编译警告,但不会停止编译
#line用于强制指定新的行号和编译文件名,并对源程序
的代码重新编号
用法
#line number filename
注:filename可省略
#line编译指示字的本质是重定义__LINE__和__FILE__
用途:无关表示部分的代码有某个人开发的,开发的那个程序段出现错误,
也就是告诉调试者的写的代码的相对位置进行检错。
现在有个更好的处理技术,来处理这个问题的。
#pragma简介
1、#pragma是编译器指示字,用于指示编译器完成一些特定的动作
2、#pragma所定义的很多指示字是编译器和操作系统特有的
3、#pragma在不同的编译器间是不可移植的
4、预处理器将忽略它不认识的#pragma指令
5、两个不同的编译器可能以两种不同的方式解释同一条#pragma指令
6、确定编程的的版本,便于差错,用于显示自己的程序的版本
提示作者版本是否正确
#pragma message("Compile Android SDK 2.0...")
深入:
1、预处理器将忽略它不认识的#pragma指令
2、 两个不同的编译器可能以两种不同的方式解释同一条#pragma指令
一般用法:
# pragma parameter
注:不同的parameter参数语法和意义各不相同
#pragma的内存对齐
构体变量是否可以直接用memcmp函数
进行相等判断?为什么?
不可以,因为内存对齐会影响到两个大小形同,可能没用的数据也进行了比较
为什么需要内存对齐?
CPU对内存的读取不是连续的,而是分成块读取的,块的大小只
能是1、2、4、8、16字节
当读取操作的数据未对齐,则需要两次总线周期来访问内存,因
此性能会大打折扣
某些硬件平台只能从规定的地址处取某些特定类型的数据,否则
抛出硬件异常
struct占用的内存大小
第一个成员起始于0偏移处
每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐
• 偏移地址和成员占用大小均需对齐
• 结构体成员的对齐参数为其所有成员使用的对齐参数的最大值
结构体总长度必须为所有对齐参数的整数倍
struct S1
{
short a;
long b;
};
struct S2
{
char c;
struct S1 d;
double e;
};
S2 的把内存大小,的结构体内用过的最大对齐数。即此刻struct S 1 对齐数的。
#运算符用于在预编译期将宏参数转换为字符串
#运算符在宏中的妙用
##运算符用于在预编译期粘连两个符号
#define STRUCT(type) typedef struct _tag_##type type; struct _tag_##type
//STRUCT(Student)// typedef struct _tag_##type type; struct _tag_##type
STRUCT(Student)
{
char* name;
int id;
};
指针的本质:
1、指针在本质上也是一个变量
2、指针需要占用一定的内存空间
3、指针用于保存内存地址的
4.不同的指针所占的大小相同,即使是void* 也是四个字节
5、指针可以访问大部分内存(低地址除外(系统使用的地址。访问这些地址都是不合法的))
6、可以以除低地址意外的任意数据为起点进行访问,当然包括自己的数据区,堆栈区,(覆盖自己的指令区,但无意义),所以指针的使用,要慎重。
7、修改以自己定义的的数据而使用的指针的类型,就要用指针指向的类型进行访问,否则访问的数据会出错。
8、例如 : int i = 4; int j = 5; int * p = (int *)((unsigned )&i +1); *p = 4;
此刻访问 visual c++ 6.0 限制这样访问,会出错,gcc,不限制,但是这样改后i的值会改变 ,(小端模式 )。
传值和传址
指针是变量,因此可以声明指针参数
1、当一个函数体内部需要改变实参的值,则需要使用指针参数
2、函数调用时实参值将复制到形参
3、指针适用于复杂数据类型作为参数的函数中
指针小结
指针是C语言中一种特别的变量
指针所保存的值是内存的地址
可以通过指针修改内存中的任意地址内容
数组
数组是相同类型的变量的有序集合
数组在一片连续的内存空间中存储元素
数组元素的个数可以显示或隐式指定
数组地址与数组名地址
数组名代表数组首元素的地址
数组的地址需要用取地址符&才能得到
数组首元素的地址值与数组的地址值相同
数组首元素的地址与数组的地址是两个不同的概念
数组名的盲点:
数组名可以看做一个常量指针,当不是指针,请注意只是代表起始地址不变的数组首地址
1、数组名“指向”的是内存中数组首元素的起始位置
2、在表达式中数组名只能作为右值使用
3、只有在下列场合中数组名不能看做常量指针
4、数组名作为sizeof操作符的参数(此时代表整个数组)
5、数组名作为&运算符的参数(此时代表整个数组)&a + 1 (unsigned int)(&a) + sizeof(*&a)
数组是一片连续的内存空间
数组的地址和数组首元素的地址意义不同
数组名在大多数情况下被当成常量指针处理
数组名其实并不是指针,在外部声明时不能混淆
数组是一段连续的内存空间
数组的空间大小为sizeof(array_type) * array_size
数组名可看做指向数组第一个元素的常量指针
(Tips
{
数组名代表的是一片空间,但是其输出地的地址值是编译器在特定区域保存的,但是他的意义是首元素的地址}
)
(T IPS 初始化的效率,比其他的都非初始化操作效率高)
不同文件下的(数组访问和指针访问区别在于,数组不需要寻址。而指针需要寻址。)
1、定义为数组,声明为指针,(会出错,把数组的内容当作地址,进行寻址进而出错,
如何纠错,把定义的指针访问方式改成指针的形式 (char*) ((unsigned int*) &p)
2、定义为指针,声明为数组 (打印乱码,因为把地址当做字符串打出了)
纠错: printf("pt = %s ",(char*)(unsigned int*p));
指针运算 p+n = (unsigned int p) +n * sizeof(*p)
指针之间只支持减法运算,且必须参与运算的指针类型必
须相同
p1 – p2; = ( (unsigned int)p1 - (unsigned int)p2) / sizeof(type);
指针也可以进行关系运算
< <= > >=
指针关系运算的前提是同时指向同一个数组中的元素
任意两个指针之间的比较运算(==, !=)无限制
下标 VS 指针
从理论上而言,当指针以固定增量在数组中移动时,其效
率高于下标产生的代码
当指针增量为1且硬件具有硬件增量模型时,表现更佳
注意:
现代编译器的生成代码优化率已大大提高,在固定增
量时,下标形式的效率已经和指针形式相当;但从可
读性和代码维护的角度来看,下标形式更优。
A与&a的区别
a为数组是数组首元素的地址
&a为整个数组的地址
a和&a的意义不同其区别在于指针运算
//a + 1 (unsigned int)a + sizeof(*a)
&a + 1 (unsigned int)(&a) + sizeof(*&a)
指针也可以进行关系运算
< <= > >=
指针关系运算的前提是同时指向同一个数组中的元素
任意两个指针之间的比较运算(==, !=)无限制
指针也可以进行关系运算
< <= > >=
指针关系运算的前提是同时指向同一个数组中的元素
任意两个指针之间的比较运算(==, !=)无限制
strncpy只复制len个字符到目标字符串
当源字符串的长度小于len时,剩余的空间以’\0’填充。
当源字符串的长度大于len时,只有len个字符会被复制,且
它将不会以’\0’结束。
strncat最多从源字符串中复制len个字符到目标串中
strncat总是在结果字符串后面添加’\0’
strncat不会用’\0’填充目标串中的剩余空间
strncmp只比较len个字符是否相等
数组类型:
1、C语言中的数组有自己特定的类型
2、数组的类型由元素类型和数组大小共同决定
定义数组类型
C语言中通过typedef为数组类型重命名
typedef type(name)[size];
数组类型:
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
数组定义:
AINT5 iArray;
AFLOAT10 fArray;
数组指针:
数组指针用于指向一个数组
数组名是数组首元素的起始地址,但并不是数组的起始地址
通过将取地址符&作用于数组名可以得到数组的起始地址
可通过数组类型定义数组指针: ArrayType* pointer;
也可以直接定义:type (*pointer)[n];
pointer为数组指针变量名
type为指向的数组的类型
n为指向的数组的大小
指针数组:
指针数组是一个普通的数组
指针数组中每个元素为一个指针
数组指针的定义:type* pArray[n];
type*为数组中每个元素的类型
pArray为数组名
n为数组大小
Main函数的参数
在执行程序的时候可以向main函数传递参数 a.exe 命令行数据
int main()
int main(int argc)
int main(int argc, char *argv[])
int main(int argc, char *argv[], char *env[])
argc – 命令行参数个数
argv – 命令行参数数组
env – 环境变量数组
总结:
数组指针本质上是一个指针
数组指针指向的值是数组的地址
指针数组本质上是一个数组
指针数组中每个元素的类型是指针
多为指针和多维数组:
1、指针变量在内存中会占用一定的空间
2、可以定义指针来保存指针变量的地址值
为什么需要指向指针的指针?
1、 指针在本质上也是变量
2、对于指针也同样存在传值调用与传址调用
3、函数要修改外部的变量必须通过传址(C++ 中可以用引用)
二维数组与二级指针
1、二维数组在内存中以一维的方式排布
2、二维数组中的第一维是一维数组
3、二维数组中的第二维才是具体的值
4、二维数组的数组名可看做常量指针
数组名的理解,类型的分析
一维数组名代表数组首元素的地址
int a[5] a的类型为int*
二维数组名同样代表数组首元素的地址
int m[2][5] m的类型为int(*)[5]
结论:
1. 二维数组名可以看做是指向数组的常量指针
2. 二维数组可以看做是一维数组
3. 二维数组中的每个元素都是同类型的一维数组
疑问:
为什么二维却不用一维
数组结论 :
C语言中只有一维数组,而且数组大小必须在编译期就作为常数确定
C语言中的数组元素可是任何类型的数据,即数组的元素可以是另一个数组
C语言中只有数组的大小和数组首元素的地址是编译器直接确定的
C语言中只会以值拷贝的方式传递参数
当向函数传递数组时
1、将整个数组拷贝一份传入函数
2、将数组名看做常量指针传数组首元素地址
3、C语言以高效为最初设计目标,在函数传递的时
4、候如果拷贝整个数组执行效率将大大下降。
二维数组参数
1、二维数组参数同样存在退化的问题
2、 二维数组可以看做是一维数组
3、 二维数组中的每个元素是一维数组
二维数组参数中第一维的参数可以省略
void f(int a[5]); void f(int a[]); void f(int* a);
void g(int a[3][3]); void g(int a[][3]); void g(int (*a)[3]);
等价关系 数组传入函数
二维数组: 数组的指针:
char a[3][4] char (*a)[4]
指针数组: 指针的指针:
int* a[5] int** a
一维数组: 指针:
float a[5] float* a
数组参数等效的指针参数 去方框,加*
C语言中无法向一个函数传递任意的多维数组 p[][] 或者 p[][][]。任意多维
为了提供正确的指针运算,必须提供除第一维之外的所有维长度 // int p[2][3] 必须提供 int p[2]
限制
一维数组参数– 必须提供一个标示数组结束位置的长度信息
二维数组参数– 不能直接传递给函数(必须写好类型 的例如: int[3]
三维或更多维数组参数– 无法使用
函数指针:
C语言中的函数有自己特定的类型
1、函数的类型由返回值,参数类型和参数个数共同决定
例:int add(int i, int j)的类型为int(int, int)
2、C语言中通过typedef为函数类型重命名
typedef type name(parameter list)
例:
typedef int f(int, int);
typedef void p(int);
回调函数是利用函数指针实现的一种调用机制
回调机制原理
调用者不知道具体事件发生的时候需要调用的具体函数
被调函数不知道何时被调用,只知道被调用后需要完成的任务
当具体事件发生时,调用者通过函数指针调用具体函数
回调机制的将调用者和被调函数分开,两者互不依赖
右左法则
1. 从最里层的圆括号中未定义的标示符看起
2. 首先往右看,再往左看
3. 当遇到圆括号或者方括号时可以确定部分类型,
并调转方向
4. 重复2,3步骤,直到阅读结束
为什么使用动态内存分配
C语言中的一切操作都是基于内存的
变量和数组都是内存的别名,如何分配这些内存由编
译器在编译期间决定
定义数组的时候必须指定数组长度
而数组长度是在编译期就必须决定的
malloc所分配的是一块连续的内存,以字节为单位,
并且不带任何的类型信息
free用于将动态内存归还系统
void* malloc(size_t size);
void free(void* pointer);
注意:
• malloc实际分配的内存可能会比请求的稍微多一点,但是不
能依赖于编译器的这个行为
• 当请求的动态内存无法满足时malloc返回NULL
• 当free的参数为NULL时,函数直接返回
malloc(0) 是不会返回NULL 的 因为NULL代表一种异常 当malloc(0)时 返回一个
合法的指针 只是由于长度是0 所以即使地址合法 你也不能使用它.
你认识malloc的兄弟吗?
void* calloc(size_t num, size_t size);
void* realloc(void* pointer, size_t new_size);
calloc的参数代表所返回内存的类型信息(数组的长度,和数组元素的大小)
calloc会将返回的内存初始化为0
realloc用于修改一个原先已经分配的内存块大小
在使用realloc之后应该使用其返回值
当pointer的第一个参数为NULL时,等价于malloc
动态内存分配是C语言中的强大功能
程序能够在需要的时候有机会使用更多的内存
malloc单纯的从系统中申请固定字节大小的内存
calloc能以类型大小为单位申请内存并初始化为0,有类型 ,
realloc用于重置内存大小
栈
栈是现代计算机程序里最为重要的概念之一
栈在程序中用于维护函数调用上下文,没有栈就没有函数,没有局部变量
栈保存了一个函数调用所需的维护信息
函数参数,函数返回地址
局部变量
函数调用上下文
堆:
为什么有了栈还需要堆?
栈上的数据在函数返回后就会被释放掉,无法传递到函数外
部,如:局部数组
堆是程序中一块巨大的内存空间,可由程序自由使用
堆中被程序申请使用的内存在程序主动释放前将一直
有效
静态存储区
程序静态存储区随着程序的运行而分配空间,直到程
序运行结束
在程序的编译期静态存储区的大小就已经确定
程序的静态存储区主要用于保存程序中的全局变量和
静态变量
与栈和堆不同,静态存储区的信息最终会保存到可执行程序中
栈,堆和静态存储区是C语言程序常涉及的三个基本内
存区
栈区主要用于函数调用的使用
堆区主要是用于内存的动态申请和归还
静态存储区用于保存全局变量和静态变量
野指针:
1、野指针通常是因为指针变量中保存的值不是一个合法(合法地址指的是 :栈空间地址,和堆空间地址。)的内存地址而造成的
2、野指针不是NULL指针,是指向不可用内存的指针(多人使用)
3、NULL指针不容易用错,因为if语句很好判断一个指针是不是NULL
4、无法判定这个指针是不是野指针。
野指针的来由:
1、局部指针变量没有被初始化(大到程序会奔溃,小到可能不发生后果,所以视为不越界,无问题,所以后果是不可预料的。)
2、使用已经释放过后的指针(写了其他人用的空间,不可释放两次,释放两次会出错。)
3、指针所指向的变量在指针之前被销毁
违法操作:
结构体成员指针未初始化
没有为结构体指针分配足够的内存
内存分配成功,但并未初始化 。(直接输出 %s)
数组越界(注意数组越界,否则引起很大的出入。)
内存泄露分析
多次指针释放(谁申请,谁释放/)
使用已释放的指针(错误);
优快云 :
以下是我对内存堆空间申请的一些个人理解:
malloc开辟空间时,并不是开辟你指定的大小,而是开辟一个内存的块信息头,再加上你的申请的空间对齐后的大小。成功后返回的指针,是你能使用的部份的起始地址,而这个起始地址减去内存块的信息头,才是真正你的开辟空间的起始。在free时,会根据你传入的指针去获取对应内存块的信息,并把释放后的内存放入空闲内存的链表,你第一次free时,修改对应存储的信息。当你第二次free时,在判断该内存块的状态,取对应的信息时,就会出错。所以会出现你遇见的问题。如果你在free时,释放的内存不是malloc返回时的地址指向空间,也会出现错误。你可以试一下char *p = malloc(20); free(p+sizeof(int));
四条规则:
1、动态申请操作必须和释放操作匹配,防止内存泄露和多次释放
2、free指针之后必须立即赋值为NULL
3、malloc申请了内存之后,应该立即检查指针值是否为NULL,防止使用值为NULL的指针
4、牢记数组的长度,防止数组越界操作,考虑使用柔性数组
面向过程的程序设计
1、面向过程是一种以过程为中心的编程思想
2、首先将复杂的问题分解为一个个容易解决的问题
3、分解过后的问题可以按照步骤一步步完成
4、函数是面向过程在C语言中的体现
5、解决问题的每个步骤可以用函数来实现
声明和定义:
1、程序中的声明可理解为预先告诉编译器实体的存在,如:变量,函数,等等
2、程序中的定义明确指示编译器实体的意义
函数参数:
函数参数
1、函数参数在本质上与局部变量相同,都是在栈上分配空间
2、函数参数的初始值是函数调用时的实参值
函数参数
函数参数的求值顺序依赖于编译器的实现!!!
C语言中大多数运算符对其操作数求值的顺
序都是依赖于编译器的实现的!!!
int i = f() * g();
顺序点:
1、程序中存在一定的顺序点
2、顺序点指的是执行过程中修改变量值的最晚时刻
3、在程序达到顺序点的时候,之前所做的一切操作必须反映
4、到后续的访问中
5、C语言会默认没有类型的函数参数为int
小结:
C语言是一种面向过程的语言
函数可理解为解决问题的步骤
函数的实参并没有固定的计算次序
顺序点是C语言中变量改变的最晚时机
函数定义时参数和返回值的缺省类型为int
可变参数函数:
C语言中可以定义参数可变的函数
参数可变函数的实现依赖于stdarg.h头文件
va_list变量与va_start (args,n), va_end(args)和va_arg(args, typename)配合使用能够访问参数值。
可变参数必须从头到尾按照顺序逐个访问
参数列表中至少要存在一个确定的命名参数
可变参数宏无法判断实际存在的参数的数量
可变参数宏无法判断参数的实际类型
警告:
va_arg中如果指定了错误的类型,那么结果是不可预测的。(会出错至少的。)
可变参数的设计总结:
可变参数是C语言提供的一种函数设计技巧
可变参数的函数提供了一种更方便的函数调用方式
可变参数必须顺序的访问
无法直接访问可变参数列表中间的参数值
活动记录
活动记录是函数调用时用于记录一系列相关信息的记录
临时变量域:用来存放临时变量的值,如k++的中间结果
局部变量域:用来存放函数本次执行中的局部变量
机器状态域:用来保存调用函数之前有关机器状态的信息,包括
各种寄存器的当前值和返回地址等;
实参数域:用于存放函数的实参信息
返回值域:为调用者函数存放返回值
参数入栈:
既然函数参数的计算次序是依赖编译器实现的,
那么函数参数的入栈次序是如何确定的呢?
调用约定
1、当一个函数被调用时,参数会传递给被调用的函数,而返回值会被返回给调用函数。函数调用约定就是描述参数
是怎么传递到栈空间的,以及栈空间由谁维护。
参数传递顺序
从右到左依次入栈:__stdcall,__cdecl,__thiscall
从左到右依次入栈:__pascal,__fastcall
调用堆栈清理
调用者清除栈。
被调用函数返回后清除栈
函数调用是C语言的核心机制
活动记录中保存了函数调用以及返回所需要的一切信息
调用约定是调用者和被调用者之间的调用协议,常用于不同开发者编写的库函数之间
调用方法:
int __stdcall add(int a,int b)
{
return a+b;
}
递归概述
递归是数学领域中概念在程序设计中的应用
递归是一种强有力的程序设计方法
递归的本质为函数内部在适当的时候调用自身
C递归函数有两个主要的组成部分:
递归点– 以不同参数调用自身
出口– 不在递归调用
C语言中的递归函数必然会使用判断语句
递归函数在需要编写的时候定义函数的出口,否则栈会溢出
递归函数是一种分而治之的思想(会把问题分解掉,使得问题变小,更容易做。)
思考题
编写一个函数打印一个字符数组的全排列?
函数设计原则:
1、不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块
2、参数名要能够体现参数的意义
void str_copy (char *str1, char *str2);
void str_copy (char *str_dest, char *str_src);
3、如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被意外修改
void str_copy (char *str_dest, const char *str_src);
4、不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型
5、在函数体的“入口处”,对参数的有效性进行检查,对指针的检查尤为重要
6、语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。
7、函数体的规模要小,尽量控制在80行代码之内
8、相同的输入应当产生相同的输出,尽量避免函数带有“记忆”功能
9、避免函数有太多的参数,参数个数尽量控制在4个以内
10、有时候函数不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值
char s[64];
int len = strlen(strcpy(s, “android”));
11、函数名与返回值类型在语义上不可冲突
char c;
c = getchar();// 返回int 类型。我们认为是char 类型,所以,我们要避免这样写函数名;
if(EOF == c)
{
//…
}
面试题1 :
#include <stdio.h>
void main()
{
int TestArray[5][5] = { {11,12,13,14,15},
{16,17,18,19,20},
{21,22,23,24,25},
{26,27,28,29,30},
{31,32,33,34,35}
};
int* p1 = (int*)(&TestArray + 1);
int* p2 = (int*)(*(TestArray + 1) + 6);
printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)),
*(*(TestArray + 3) + 3), p1[-8],
p2[4]);
}
面试题2 :
#include<stdio.h>
void main()
{
char* p = "hello world!";
int a = (int)p;
short s = 'c';
printf("%c\n", (long)(*((int*)p)));
printf("%s\n", a);
printf("%s\n", &s);
}
输出:
(注课程源于国嵌C语言学习,及个人学习体会)