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 { p[i] = i; printf("%d\n", p[i]); } ret = 1; }while(0); free(p); return ret; } break和continue的区别 break表示终止循环的执行 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; }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 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步骤,直到阅读结束 为什么使用动态内存分配 变量和数组都是内存的别名,如何分配这些内存由编 译器在编译期间决定 定义数组的时候必须指定数组长度 而数组长度是在编译期就必须决定的 malloc所分配的是一块连续的内存,以字节为单位, 并且不带任何的类型信息 free用于将动态内存归还系统 void* malloc(size_t size); void free(void* pointer); 注意: • malloc实际分配的内存可能会比请求的稍微多一点,但是不 能依赖于编译器的这个行为 • 当请求的动态内存无法满足时malloc返回NULL • 当free的参数为NULL时,函数直接返回 你认识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用于重置内存大小 栈 栈在程序中用于维护函数调用上下文,没有栈就没有函数,没有局部变量 栈保存了一个函数调用所需的维护信息 函数参数,函数返回地址 局部变量 函数调用上下文 堆: 为什么有了栈还需要堆? 栈上的数据在函数返回后就会被释放掉,无法传递到函数外 部,如:局部数组 堆是程序中一块巨大的内存空间,可由程序自由使用 堆中被程序申请使用的内存在程序主动释放前将一直 有效 静态存储区 序运行结束 在程序的编译期静态存储区的大小就已经确定 程序的静态存储区主要用于保存程序中的全局变量和 静态变量 与栈和堆不同,静态存储区的信息最终会保存到可执行程序中 存区 栈区主要用于函数调用的使用 堆区主要是用于内存的动态申请和归还 静态存储区用于保存全局变量和静态变量 野指针: 1、野指针通常是因为指针变量中保存的值不是一个合法(合法地址指的是 :栈空间地址,和堆空间地址。)的内存地址而造成的 2、野指针不是NULL指针,是指向不可用内存的指针(多人使用) 3、NULL指针不容易用错,因为if语句很好判断一个指针是不是NULL 4、无法判定这个指针是不是野指针。 野指针的来由: 1、局部指针变量没有被初始化(大到程序会奔溃,小到可能不发生后果,所以视为不越界,无问题,所以后果是不可预料的。) 2、使用已经释放过后的指针(写了其他人用的空间,不可释放两次,释放两次会出错。) 3、指针所指向的变量在指针之前被销毁 违法操作: 结构体成员指针未初始化 没有为结构体指针分配足够的内存 内存分配成功,但并未初始化 。(直接输出 %s) 数组越界(注意数组越界,否则引起很大的出入。) 内存泄露分析 多次指针释放(谁申请,谁释放/) 使用已释放的指针(错误); CSDN : 以下是我对内存堆空间申请的一些个人理解: 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 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 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语言学习,及个人学习体会)
switch能否用continue关键字?为什么?
函数的参数
int arr[];
‚如何申请空间
实用注意事项:
C语言中的一切操作都是基于内存的
malloc(0) 是不会返回NULL 的 因为NULL代表一种异常 当malloc(0)时 返回一个
合法的指针 只是由于长度是0 所以即使地址合法 你也不能使用它.
栈是现代计算机程序里最为重要的概念之一
程序静态存储区随着程序的运行而分配空间,直到程
栈,堆和静态存储区是C语言程序常涉及的三个基本内