一、数据类型
1、基本数据类型
数据类型是创建变量的模型。变量名是连续存储空间的别名,程序中使用变量命名存储空间,通过变量可以使用存储空间。变量所占的内存大小取决于创建变量的数据类型。
2、有符号和无符号
有符号数中数据类型的最高位用于标识数据的符号,最高位为1表示为负数,最高位为0表示为正数。
计算机中有符号数通常使用补码表示,正数的补码为正数本身,负数的补码为负数的绝对值的各位取反后加1。
计算机中无符号数通常使用原码表示,无符号数默认为正数,没有符号位。对于无符号数来说,MAX_VALUE + 1等于MIN_VALUE,MIN_VALUE - 1等于MAX_VALUE。
无符号数与有符号数进行混合运算时,会将有符号数转换为无符号数后再进行计算,结果为无符号数。
整型常量的类型
3、浮点数的实现
float与double类型的数据在计算机中的表示方法相同,由于所占存储空间不同,分别能够表示的数值范围和精度不同。
浮点数在计算机内部存储方式分为三段,符号位、指数、尾数
浮点数的转换需要先将浮点数转换为二进制,将得到的二进制浮点数使用科学计数法表示,根据数据类型计算偏移后的指数。指数的偏移量和数据类型有关,float类型加127,double类型加1023。
8.25的float表示如下:
8.25的二进制表示:1000.01==>1.00001(2^3)
符号位:0
指数:127+3 = 130 = b10000010
小数:00001
8.25的float表示为:0 10000010 00001 000000000000000000 = 0x41040000
由于float和int类型都占四个字节,float能表示的具体数字的个数与int相同,但是float表示的数值时不连续的,不能作为精确数使用,因此float类型的表示范围比int类型表示的范围大。Double类型与float类型在计算机中的表示方法相同,但是double类型占用的存储空间大,因此所能表示的精度更高。
4、类型转换
C语言中的数据类型可以进行转换,包括显示类型转换和隐式类型转换。
强制类型转换时,如果目标类型能够容纳目标值,则转换后结果不变;如果目标类型不能容纳目标值,则结果将产生数据截断。
隐式类型转换是编译器进行的类型转换,低类型到高类型的隐式转换是安全的,结果不会产生数据截断;高类型到低类型的隐式转换是不安全的,结果产生类型截断,结果可能是不正确的。
隐式类型转换发生的时机:
A、算术运算中,低类型转换为高类型
B、赋值表达式中,表达式的值转换为左值的类型
C、函数调用时,实参转换为形参的类型
D、函数返回时,return表达式转换为返回值类型
5、void类型
void修饰函数返回值和参数
void不能用于定义变量,标准C语言编译器中sizeof(void)会报错,GCC编译器中因为进行了扩展,不会报错,结果为1。
二、程序结构
1、分支语句
if...else
if语句根据条件选择执行语句,else不能独立存在,但是总是和离它最近的if匹配。
float变量不能直接和0进行比较,需要确定变量在某个较小的区间内。
if(-0.0000001 < a && a < 0.0000001)
switch
switch语句对应单个条件多个值的情况,一般情况下case语句需要有break,否则会导致分支重叠。default分支需要加上,用于处理特殊情况。
case语句中的值只能是整型或char类型。
2、循环语句
do语句先执行后判断,循环体至少执行一次
do
{
}while();
while语句先判断后执行,循环体可能不执行
while()
{
}
for语句先判断后执行
for(; ; )
break表示终止循环的执行
continue表示中止本次循环,进入下一次循环
三、const和volatile
1、const
const修饰的变量告诉编译期变量是只读的,但还是变量,向编译器指明变量不能做左值。
const修饰的局部变量在栈上分配空间
const修饰的全局变量在全局数据区分配空间
const只在编译期有效,在运行期无效
#include <stdio.h>
const int g = 10;
int main(int argc, char **argv)
{
const int c = 1;
int *p = (int *)&c;
*p = 100;
printf("%d\n", c);
p = (int *)&g;
*p = 1000;
printf("%d\n", c);
return 0;
}
编译正常,运行时对使用指针可以对const局部变量进行修改,但使用指针对const全局变量修改时发生段错误。
标准C语言编译器不会将const修饰的全局变量存储于只读存储区,而是存储在可以修改的全局数据区,其值可以通过指针修改。但是现代编译器如GCC将const全局变量存储在只读存储区,通过指针修改其值将发生段错误。const volatile修饰则可以将全局变量存储在全局数据区,其值可以修改。
const修饰函数参数表示在函数体内部不希望修改参数的值
const修饰函数返回值表示返回值不可改变
#include <stdio.h>
const volatile int g = 10;
int main(int argc, char *argv[])
{
const int i = 0;
int *p = (int *)&i;
*p = 100;
printf("%d %d\n", i, *p);
p = (int *)&g;
*p = 1000;
printf("%d %d\n", g, *p);
return 0;
}
2、volatile
volatile指示编译器不能优化,必须每次到内存中取变量的值,主要修饰被多个线程访问的变量、或是被未知原因更改的变量
const volatile int i = 10;
作为局部变量,i不可以做左值,但可以使用指针修改值,编译器不对变量i进行优化,每次都到内存取值。
四、struct和union
1、struct
柔性数组是数组大小待定的数组
结构体中最后的成员可以是柔性数组,柔性数组只是一个标识符,不占存储空间。
2、union
union只分配最大成员的空间,所有成员共享这个空间。
union的使用会受到计算机大小端的影响
使用union测试计算机大小端的代码如下:
#include <stdio.h>
void big_little(void);
int main(int argc, char *argv[])
{
big_little();
return 0;
}
void big_little(void)
{
union Mode
{
char c;
int i;
};
union Mode m;
m.i = 1;
if(m.c)
{
printf("little\n");
}
else
{
printf("big\n");
}
}
五、enum、sizeof、typedef
1、enum
enum是C语言中的一种自定义类型,enum的值是自定义的整型值,第一个定义的enum值默认为0,默认情况下enum的值是前一个定义的值加1,也可以指定。enum中定义的值是C语言中真正的常量
2、sizeof
sizeof是编译器的内置指示符,用于计算类型或变量所占内存大小,sizeof的值在编译期就已经确定。
int var = 0;
int size = sizeof(var++);
var并不会执行var++,而是在编译时就将sizeof(var++替换为值4
3、typedef
typedef用于给一个已经存在的类型进行重命名,本质上不产生新类型
typedef重命名的源类型可以在typedef语句后定义,typedef不能使用unsigned、signed修饰。
六、接续符、单引号、双引号
1、接续符
C语言中接续符(\)可以指示编译器,编译器会将接续符删除,跟在接续符后面的字符自动接续到前一行。在接续单词时,接续符后面不能有空格,接续符的下一行之前也不能有空格。通常接续符使用在定义宏代码块时使用。
2、单引号和双引号
C语言中单引号用来表示字符字面量,双引号用来表示字符串字面量
‘a’表示字符字面量,占一个字节大小,’a’+1表示’b’,”a”表示字符串字面量,”a”+1表示指针运算,结果为指向”a”字符串的结束符’\0’。
char c = “hello”;
字符串字面量“hello”的地址赋值给字符变量c,由于地址占用四个字节空间,赋值给字符类型后会发生类型截断。
七、++、--、三目运算符
1、++、--
++、--参与混合运算的结果是不确定的。
2、三目运算符
三目运算符返回变量的值,而不是变量本身。根据隐式类型转换规则确定返回值类型。
int a = 1;
int b = 2;
int c = 0;
c = a < b ? a : b;
(a < b ? a : b) = 3;//error
*(a < b ? &a : &b) = 3;//ok
八、宏
1、宏定义
#define由预处理器处理,直接进行文本替换,不会进行语法检查,#define定义的宏可以出现在程序的任意位置
#define定义的宏常量本质为字面量
宏由预处理器处理,编译器不知道宏的存在。
宏表达式没有任何的调用开销,不能出现递归定义
编译器内置的宏:
__FILE__:被编译的文件名
__LINE__:当前行号
__DATE__:编译时的日期
__TIME__:编译时的时间
__STDC__:编译器是否遵循标准C规范
#define LOG(s) printf("[%s] File:%s, Line:%d %s \n", __DATE__, __FILE__, __LINE__, s)
2、条件编译
条件编译是预编译指示命令,用于控制是否编译某段代码。预编译器根据条件编译指令有选择的删除代码,编译器不知道代码分支的存在。
条件编译可以解决头文件重复包含的编译错误
#ifndef _FILE_H_
#define _FILE_H_
//source code
#endif
条件编译通过不同的条件编译不同的代码,生成不同条件的目标,实际工程中可以使用条件编译将同一份工程代码生成不同的产品线或是区分产品的调试和发布版。
3、#error
#error用于生成一个编译错误消息
语法:#error message
message不需要双引号
#ifndef __cplusplus__
#error This file should be processed with C++ compiler.
#endif
编译过程中产生错误信息意味着编译将终止,无法生成最终的可执行程序
4、#line
#line用于强制指定新的行号和编译文件名,并对源程序的代码进行重新编号
#line number filename
#line本质上是对__LINE__和__FILE__宏的重定义
5、#pragma
#pragma用于指示编译器完成某些特定的动作,所定义的很多关键字和编译器有关,在不同编译器间是不可以移植的。预处理器将忽略不认识的#pragma指令,不同的编译器可能会对#pragma指令的解释不同。
#pragma message
编译时输出消息到编译器输窗口,用于提示信息
#if defined(ANDROID20)
#pragma message("Compile Android SDK 2.0...")
#define VERSION "Android 2.0"
#elif defined(ANDROID23)
#pragma message("Compile Android SDK 2.3...")
#define VERSION "Android 2.3"
#elif defined(ANDROID40)
#pragma message("Compile Android SDK 4.0...")
#define VERSION "Android 4.0"
#else
#error Compile Version is not provided!
#endif
与#error不同,#pragma massage仅代表一条编译消息,不代表编译出错
#pragma once
#pragma once用于保证头文件只被编译一次,和编译器相关,编译器不一定支持。
工程代码使用如下:
#ifndef _FILE_H_
#define _FILE_H_
#pragma once
#endif
#pragma pack
#pragma pack用于指定内存对齐方式,一般成对使用
#pragma pack(n)
//source code
#pragma pack()
6、#运算符
#运算符用于在预处理期将宏参数转换为字符串,只能在宏定义中有效
#define STRING(x) #x
printf("%s\n", STRING(Hello world!));
#define CALL(f, p) (printf("Call function %s\n", #f), f(p))
7、##运算符
##运算符用于在预处理期粘连两个标识符,只在宏定义中有效
#define NAME(n) name##n
int main()
{
int NAME(1);
int NAME(2);
NAME(1) = 1;
NAME(2) = 2;
printf("%d\n", NAME(1));
printf("%d\n", NAME(2));
return 0;
}
九、main函数
main函数是操作系统调用的函数,操作系统将main函数作为应用程序的入口,将main函数的返回值作为程序的退出状态。
现代编译器支持在main函数前调用其他的函数。
#include <stdio.h>
#ifndef __GNUC__
#define __attribute__(x)
#endif
__attribute__((constructor))
void before_main()
{
printf("%s\n",__FUNCTION__);
}
__attribute__((destructor))
void after_main()
{
printf("%s\n",__FUNCTION__);
}
int main()
{
printf("%s\n",__FUNCTION__);
return 0;
}
十、程序内存布局
不同代码在可执行程序中的对应关系:
堆栈在程序运行后才在正式存在,是程序运行的基础。
.bss段存放未初始化的全局变量和静态变量
.text段存放程序的可执行代码
.data段存放已经初始化的全局变量和静态变量
.rodata段存放程序中的常量值,如字符串字面
静态存储区通常指.data段和.bss段
只读存储区通常指.rodata段
代码段通常指.text段