一、基本数据类型分析
什么是数据类型
1、数据类型可以理解为固定内存大小的别名
2、数据类型是创建变量的模子
类型的本质:
char 是1byte的别名,short是2byte的别名,int是4byte的别名。
char c在内存中创建了一个1byte的变量c,short s在内存中创建了一个2byte的变量s,int i在内存中创建了一个4byte的变量i。
变量的本质:
1、变量是一段实际连续存储空间的别名
2、程序中通过变量来申请并命名存储空间
3、通过变量的名字可以使用存储空间
实例分析
#include
int main()
{
char c = 0;
short s = 0;
int i = 0;
printf("%d, %d\n", sizeof(char), sizeof(c));
printf("%d, %d\n", sizeof(short), sizeof(s));
printf("%d, %d\n", sizeof(int), sizeof(i));
return 0;
}
运行结果为:
1, 1
2, 2
4, 4
自定义类型,创建变量实例分析:
思维导图:
#include
#include
typedef int INT32;
typedef unsigned char BYTE;
typedef struct _demo
{
short s;
BYTE b1 = a;
BYTE b2;
INT32 i;
}DEMO;
int main()
{
INT32 i32;
BYTE byte;
DEMO d;
printf("%d, %d\n", sizeof(INT32), sizeof(i32));
printf("%d, %d\n", sizeof(BYTE), sizeof(byte));
printf("%d, %d\n", sizeof(DEMO), sizeof(d));
return 0;
}
运行结果:
4, 4
1, 1
8, 8
二、auto,register,static分析
1、C语言中的变量可以有自己的属性
2、在定义变量的时候可以加上“属性”关键字
3、“属性”关键字指明变量的特有意义
auto即C语言中局部变量的默认属性
编译器默认所有的局部变量都是auto的
4、static关键字指明变量的“静态”属性
5、static关键字同时具有“作用域限定符”的意义
static修饰的局部变量存储在程序静态区
static的另一个意义是文件作用域标示符
——static修饰的全局变量作用域只是声明的文件中,所以static也称为文件作用域标示符
——static修饰的函数作用域只是声明的文件中。
static不可以修饰递归函数的返回变量,如果强制修饰的话,会占用很大的静态数据区,直到程序运行结束才会释放。
static用于修饰局部变量,并没有改变它的作用域,但改变了它的生存周期,使其在程序运行期间一直存在。
6、register关键字指明将变量存储于寄存器中
7、register只是请求寄存器变量,但不一定请求成功
register变量必须是CPU寄存器可以接受的值
不能用&运算符获取register变量的地址,因为&运算符,取得是内存地址,而register变量是放在了寄存器中,这就产生了矛盾。
register变量用于实时的操作系统中或对运行速度要求比较高的并重复使用的变量。
register只能修饰局部变量,因为对于全局变量来说,它分配在静态存储区,这就产生了矛盾。
平时我们口中的变量地址是指内存中的地址,多以register变量不可以进行&运算。
8、小结:
auto变量存储在程序的栈中,默认属性
static变量存储在程序静态区中
register变量请求存储于CPU寄存器中
实例代码:
main.c
#include
void f1()
{
int i = 0;//存在栈中,函数调用结束就会释放 。且每次都会进行初始化。
i ++;
printf("%d\n", i);
}
void f2()
{
static int i = 0;//静态数据区分配空间,函数调用结束,不会释放 。它只初始化一次。它作用域仅仅在函数中,但它的生命周期却是整个程序的运行周期。
i ++;
printf("%d\n", i);
}
extern int ff(); //引入外部函数
int main()
{
auto int i = 0;
register int j = 0;//变量存储在寄存器中,不能对其取地址,因为取地址运算符取到的是内存地址。
static int k = 0; //静态数据区
printf("%0x\n", &i);
// printf("%0x\n", &j);//报错,因为我们取地址默认为内存地址,所以这里矛盾
printf("%0x\n", &k);
for(i = 0; i < 5; i++)
{
f1();
}
printf("\n下面使用的是静态局部变量,变量只初始化一次\n");
for(i = 0; i < 5; i++)
{
f2();
}
printf("\nren = %d", ff());
return 0;
}
ren.c
static int ren = 24; //限定了ren的范围了
static int r_ren()//函数名不要和文件名相同
{
return ren;
}
int ff()
{
r_ren();
}
运行结果:
三、if,switch,do,while,for分析
1、if语句用于根据条件选择执行语句
2、else不能独立存在且总是与它最近的if相匹配
3、else语句后可以接连其他if语句
if( condition )
{
//statement 1
}
else
{
//statement 2
}
4、if语句中零值比较的注意点
#bool型变量应该直接出现于条件中,不要进行比较
在c语言中并不是用0和1来区分假与真,而是用非零代表真。0代表假。即-1为真。
bool b = TRUE;
if( b )
{
//statement 1
}
else
{
//statement 2
}
#普通变量和0值比较时,0值应该出现在比较符号左边
否则一旦将判断语句错写成赋值语句,对于一大型软件无疑是灾难性的。
int i = 1;
if( 0 == i )
{
//statement 1
}
else
{
//statement 2
}
#float型变量不能直接进行0值比较,需要定义精度
在c语言中,float,double在内存中是离散地展示的。计算机利用离散的量来表示浮点型的值。所以用定义精度来解决上面的问题。有的时候不这样,直接判断也可以。那是因为现在的一些编译器对C做了修改和加强。但为了提高可移植性,就要使用精度。
#define EPSINON 0.00000001
float f = 0.0;
if( (-EPSINON <= f) && (f <= EPSINON))
{
//statement 1
}
else
{
//statement 2
}
if的同胞兄弟switch
5、switch语句对应单个条件多个分支的情形
6、每个case语句分支必须要有break,否则会导致分支重叠
7、default语句有必要加上,以处理特殊情况
switch(表达式)
{
case 常量:
代码块
case 常量:
代码块
default:
代码块
}
8、case语句中的值只能是整型或字符型,所以对于块判断的要使用if语句,而不能使用switch。
也就是说switch只能判断离散的值,而if可以用来判断离散值也可以用来判断连续的范围。但switch对于离散值,看起来更易读,而且编译后的代码更加紧凑。
9、case语句排列顺序分析
按字母或数字顺序排列各条语句
正常情况放在前面,异常情况放在后面
default语句只用于处理真正的默认情况
if与switch对比代码:
#include
void f1(int i)
{
if( i < 6 )
{
printf("Failed!\n");
}
else if( (6 <= i) && (i <= 8) )
{
printf("Good!\n");
}
else
{
printf("Perfect!\n");
}
}
void f2(char i)
{
switch(i)
{
case 'c':
printf("Compile\n");
break;
case 'd':
printf("Debug\n");
break;
case 'o':
printf("Object\n");
break;
case 'r':
printf("Run\n");
break;
default:
printf("Unknown\n");
break;
}
}
int main()
{
f1(5);
f1(9);
f2('o');
f2('d');
f2('e');
}
运行结果:
Failed!
Perfect!
Object
Debug
Unknown
小结:
1、if语句适用于需要“按片”进行判断的情形中
2、switch语句适用于需要对各个离散值进行分别判断的情形中
3、if语句可以安全从功能上代替switch语句,但switch语句无法代替if语句
4、switch语句对于多分支判断的情形更加简洁
循环语句分析:
循环语句的基本工作方式:
1、通过条件表达式判定是否执行循环体
2、条件表达式遵循if语句表达式的原则
do,while,for的区别
1、do语句先执行后判断,循环体至少执行一次
2、while语句先判断后执行,循环体可能不执行
3、for语句先判断后执行,相比while更简洁
三种循环语句使用对比,累加自然数
#include
int f1(int n)
{
int ret = 0;
int i = 0;
for(i=1; i<=n; i++)//很直观
{
ret += i;
}
return ret;
}
int f2(int n)
{
int ret = 0;
while( (n > 0) && (ret += n--) );//不直观
return ret;
}
int f3(int n)
{
int ret = 0;
if( n > 0 )//如果缺少判断,就会进入死循环
{
do
{
ret += n--;
}while( n );
}
return ret;
}
int main()
{
printf("%d\n", f1(10));
printf("%d\n", f2(10));
printf("%d\n", f3(10));
}
运行结果:
55
55
55
break和continue的区别
1、break表示终止循环的执行
2、continue表示终止本次循环体,进入下次循环执行
在C语言中,break表示跳出一个块语句,也就大括号括起来的语句。所以break可以用在switch中,而continue是跳出本次循环,接着执行判断语言是否下一次循环。而switch没有循环,所以不可以使用continue。
可总结如下:循环中可以用break和continue。但switch中无循环,就不 可以使用contine,可以使用break跳出块语句。
do 与 break的妙用与一般函数设计的思维导图:
#include
#include
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);
//只执行一次,形成块语句,便于break跳出后执行free。这是一个巧妙的应用。这样设计使得函数有一个入口,一个出口。所以对于安全性检测比较多的函数,可以考虑使用do——while语句和break语句实现块封装。
free(p);
return ret;
}
int main()
{
if( func(10) )
{
printf("OK");
}
else
{
printf("ERROR");
}
}
关于free语句的分析:free不对指针的值做任何操作,所以free(p)后,p的值不变。而是试图改变指针指向的一片连续的存储器空间的状态。如果这片存储空间是malloc函数分配的,那就释放这片空间,释放后的空间可以再次分配。如果指针本来就等于NULL,则调用free不会有任何操作。因为NULL不指向任何有效地址。建议free之后将P赋值NULL避免二次释放,是系统崩溃。如果不释放就会导致内存泄露。
那什么是内存泄露呢?
内存泄露是指操作系统可提供给所有进程的存储空间正在被某个进程榨干,最终结果是程序运行时间越长,占用存储空间愈来愈多,最终用尽全部存储空间,整个系统就崩溃了。
而“内存泄露”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是虚拟内存,这个虚拟内存大小取决于磁盘交换区的设定大小,由程序申请的块内存且没有任何一个指针指向它,那么内存就泄露了。
四、goto, void, extern, sizeof分析
遭人遗弃的goto
1、高手潜规则:禁用goto
2、项目经验:程序质量与goto的出现次数成反比
3、最后的判决:将goto打入冷宫
goto副作用的分析代码
#include
void func(int n)
{
int* p = NULL;
if( n < 0 )
{
goto STATUS;//它破坏了顺序执行,有可能跳过了重要的代码,这里是给p分配空间的代码段。
}
p = malloc(sizeof(int) * n);
STATUS:
p[0] = n;
}
int main()
{
f(1); //程序可以正常执行
f(-1); //由于程序跳过了给p指针分配内的语句,所以运行结果出现错误了。
return 0;
}
void的意义
1、void修饰函数返回值和参数
#如果函数没有返回值,那么应该将其声明为void型
#如果函数没有参数,应该声明其参数为void
void修饰函数返回值和参数仅为了表示无
#不存在void变量
c语言中没有定义void究竟是多大内存的别名,没有void的标尺无法在内存中裁剪出void对应的变量;所以void称为了c语言中的灰色地带,而有些编译器厂家自己定义了void的大小,如gcc就定义void大小为1,但g++不允许出现void变量。
#void 指针的含义:
C语言中规定只有相同类型的指针才可以相互赋值
void* 指针作为左值用于“接收”任意类型的指针
void* 指针作为右值赋值给其它指针时需要强制类型转换
int *pI = (int *)malloc(sizeof(int));
char * pC =(char *)malloc(sizeof(char));
void * p = NULL;
int * pni = NULL;
char *pnc = NULL;
p = pI;
pni = p; //error
p = pC;
pnC = p; //error
使用void *指针实现memset函数
思维导图:
#include
void *mem_set(void *p, char v, int size)//void*函数能够接受任意类型的地址值,无论是字符还是整型。
{
void *ret = p;
char *dest = (char*)p;
int i = 0;
for(i=0; i
dest[i] = v;
}
return ret;
}
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int i = 0;
for(i=0; i<5; i++)
{
printf("%d\n", a[i]);
}
mem_set(a, 0, sizeof(a));
for(i=0; i<5; i++)
{
printf("%d\n", a[i]);
}
return 0;
}
运行结果
全部清零。
extern中隐藏的意义
#extern用于声明外部定义的变量和函数
#extern用于“告诉”编译器用C方式编译
C++编译器和一些变种的C编译器默认会按“自己”的方式编译函数和变量,通过extern关键字可以命令编译器“以标准C方式进行编译”。
extern "C"
{
int f(int a, int b)
{
return a+b;
}
}
extern 的使用分析:
// test.c
#include
extern int g;
extern int get_min(int a, int b);
int main()
{
printf("%d\n", g);
printf("%d\n", get_min(5, 1));
return 0;
}
// test2.c
int g = 100;
int get_min(int a, int b)
{
return (a < b) ? a : b;
}
运行结果:
100
1
为sizeof正名:
1、sizeof是编译器的内置指示符,不是函数
2、sizeof用于“计算”相应实体所占的内存大小
3、sizeof的值在编译期就已经确定
sizof的代码分析:
#include
int main()
{
int a;
printf("%d\n", sizeof(a)); //使用了括号是为了统一写法
printf("%d\n", sizeof a); //函数不可能这样使用
printf("%d\n", sizeof (int)); //函数的参数不可能是数据类型
return 0;
}
运行结果:
4
4
4