目录
知识比较零碎,但都是开发中要用的基础知识,很实用。
基本数据类型
sizeof :为了得到某个类型或某个变量在特定平台上的准确大小,表达式 sizeof(type) 得到对象或类型的存储字节大小。
long int 其实就是长整型long 可以省去int 。在标准中,规定 int至少和short一样长,long至少和int一样长。
long和int在早期16位电脑时候, int为 2字节,long 为4字节,而计算机发展到现在,一般32、64下,long和int一样。和java类比的话,java的long就是 long long 8字节。
C99标准以前,C语言里面是没有bool,C++里面才有, C99标准里面定义了bool类型,需要引入头文件stdbool.h bool类型有只有两个值:true =1 、false=0。 因此实际上bool就是一个int 所以在c/c++中 if 遵循一个规则, 非0为true,非空为true; NULL 其实也就是被define为了 0。
格式化
include <stdio.h>之后可以使用printf、sprintf等,其中有格式化的书写,各种数据类型的书写在基础类型小节中的表格里有说明。
例如:8进制 %o ,16进制小写: %x、大写:%X ,(0x)+16进制前面 %#x。
内存和布局
动态内存申请
栈的内存是有限制的,如果我们定义数组的空间的时候超出了栈的限制,则会保栈溢出的异常。通常程序员可以在堆中申请内存。
当数据无法确定或者比较庞大,在堆中需要使用动态内存申请:
int *di1 = (int*)malloc(1 * 1024 * 1024);
malloc动态申请内存应该紧跟memset 初始化内存全为0:
memset(di1, 0, 1 * 1024 * 1024);
而下面的calloc申请内存并将内存初始化为null:
int *di2 = (int*)calloc(10, sizeof(int));
对malloc申请的内存进行大小的调整:
realloc(di1, 20 * sizeof(int));
新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;如果新的大小小于原内存大小,可能会导致数据丢失。并养free的好习惯,将指针置为 null:
if (di1) {
free(di1);
di1 = 0;
}
if (di2) {
free(di2);
di2 = 0;
}
物理内存和虚拟内存
物理内存:指通过物理内存条而获得的内存空间;虚拟内存:是一种内存管理技术,电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大,则会导致内存消耗殆尽,虚拟内存技术还会匀出一部分硬盘空间来充当内存使用。下面是一张描述虚拟内存的分布图。
代码段:存放程序执行代码(cpu要执行的指令)。栈是向低地址扩展数据结构,堆是向高地址扩展数据结构。进程分配内存主要由两个系统调用完成:brk和mmap 。brk是将_edata(指带堆位置的指针)往高地址推;mmap 找一块空闲的虚拟内存。通过glibc (C标准库)中提供的malloc函数完成内存申请,malloc小于128k的内存,使用brk分配内存,将_edata往高地址推,大于128k则使用mmap。
指针
指针是一个变量,存放的是地址,称为该变量指向了一个内存地址。声明指针或者不再使用后都要将其置为0 (NULL)。以下3中写法都是行得通的:
int *a; 正规
int* a;
int * a;
野指针:未初始化的指针,例如以上;悬空指针:指针最初指向的内存已经被释放了的一种指针,释放的意思是标记为空闲,是可再利用的,系统可以再用这块内存,等到系统重用了这块内存时,这个指针就悬空了,指向了一个不可描述的区域。
32位系统中指针占用4字节,64位为8字节。
//声明一个整型变量
int i = 10;
//将i的地址使用取地址符给p指针
int *p = &i;
sizeof(p) == 4;
sizeof(p) == 8;
解引用: 返回指针变量指向的该内存地址处保存的值。
int i = 10;
int *p = &i;
//解引用
//p指向一个内存地址,使用*解出这个内存地址处的值 即为 10
int pv = *p;
数组和指针
指针和数组名都表示地址。数组是一块内存连续的数据;指针是一个指向内存空间的变量。
数组的指针,例如:
//二维数组类型是 int (*p)[x]
int array[2][3] = { {11,22,33},{44,55,66} };
//也可以 int array[2][3] = {11,22,33 ,44,55,66 };
//array1 就是一个 int[3] 类型的指针
int (*array1)[3] = array;
怎么取 55 ?通过下标取:array[1][1] == array1[1][1] ;通过解引用取:int i = *(*(array1 + 1) + 1);进一步解释如下:
先拆分,因为array1的类型是3个int元素的数组指针,所以array1 + 1是移动了12位字节;再获得{44,55,66这个}数组*(array1 + 1),*(array1 + 1)相当于一个3个int元素的数组的数组名;最后,获得 55,(*(array1 + 1))[1],或者先获得55的地址,*(array1 + 1) + 1,再解引用,*(*(array1 + 1) + 1)。
指针数组,例如:
int *array2[2];
array2[0] = &i;
array2[1] = &j;
多级指针
指向指针的指针。例如:
int a = 10;
int *i = &a;
int **j = &i;
// *j 解出i为a变量的地址值,**j解出*i值为a变量存放的值10
printf("%d\n", **j);
const常量
等同于java中放入final,一句话总结,const不修饰星号,从右往左看,const修饰谁谁就不可变,我们看如下的代码:
//例如P是一个指针,指向char类型,且char类型的值不可变
char str[] = "hello";
const char *p = str;
str[0] = 'c'; //正确,却可以通过数组名修改
p[0] = 'c'; //错误,不能通过指针修改 const char
//也可以修改指针指向的数据
p = "12345";
// const不修饰星号,所以性质和 const char * 一样
char const *p1;
//p2是一个const指针,指向char类型数据
char * const p2 = str;
p2[0] = 'd'; //正确
p2 = "12345"; //错误
//p3是一个const的指针变量 意味着不能修改它的指向
//同时指向一个 const char 类型 意味着不能修改它指向的字符
char const* const p3 = str;
const char * const p3 = str;
函数
函数由函数头与函数体构成,和java一样,声明在使用之前。
函数参数
传值给形参,把参数的值复制给函数的形式参数,修改形参不会影响实参。传递引用给形参,形参为指向实参地址的指针,可以通过指针修改实参。例如:
void change1(int *i) {
*i = 10;
}
void change2(int *i) {
*i = 10;
}
int i = 1;
change1(i);
printf("%d\n",i); //i == 1
change2(&i);
printf("%d\n",i); //i == 10
//该函数交换的是指针变量,并没有交换指针指向的值
void change3(int *i, int *j) {
int *temp = i;
i = j;
j = temp;
}
可变参数
与Java一样,C当中也有可变参数。它的使用通过va_start,va_arg,va_end等与参数有关的函数联合使用。例如:
#include <stdarg.h>
int add(int num, ...)
{
va_list valist;
int sum = 0;
// 初始化 valist指向第一个可变参数 (...)
va_start(valist, num);
for (size_t i = 0; i < num; i++)
{
//访问所有赋给 valist 的参数
int j = va_arg(valist, int);
printf("%d\n", j);
sum += j;
}
//清理为 valist 内存
va_end(valist);
return sum;
}
函数指针
函数指针是指向函数的指针变量。例如:
void println(char *buffer) {
printf("%s\n", buffer);
}
//接受一个函数作为参数
void say(void(*p)(char*), char *buffer) {
p(buffer);
}
void(*p)(char*) = println;
p("hello");
//传递参数
say(println, "hello");
//typedef 创建别名 由编译器执行解释
//typedef unsigned char u_char;
typedef void(*Fun)(char *);
Fun fun = println;
fun("hello");
say(fun, "hello");
//类似java的回调函数
typedef void(*Callback)(int);
void test(Callback callback) {
callback("成功");
callback("失败");
}
void callback(char *msg) {
printf("%s\n", msg);
}
test(callback);
预处理器
预处理器不是编译器,但是它是编译过程中一个单独的步骤,预处理器是一个文本替换工具,所有的预处理器命令都是以井号(#)开头。
常用预处理器有:
宏,就是文本替换,#define,#ifdef,#ifndef,#undef等都是用来处理宏的。宏变量一般使用大写区分,例如:
#define A 1
我们还可以定义宏函数:
#defind test(i) i > 10 ? 1: 0
宏中连接符#的使用,用来连接两个符号组成新符号:
#define DN_INT(arg) int dn_ ## arg//d定义整形变量dn_arg
DN_INT(i) = 10;
dn_i = 100;
宏的定义中字符串太长可以用反折号:
#define PRINT_I(arg) if(arg) { \
printf("%d\n",arg); \
}
PRINT_I(dn_i);
//可变宏
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"NDK", __VA_ARGS__);
#define MULTI(x,y) x*y
//获得 1+1*2 = 3
printf("%d\n", MULTI(1+1, 2));
宏函数
优点:文本替换,每个使用到的地方都会替换为宏定义。 不会造成函数调用的开销(开辟栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈)。
缺点:生成的目标文件大,不会执行代码检查。
内联函数
和宏函数工作模式相似,但是两个不同的概念,首先是函数,那么就会有类型检查同时也可以debug在编译时候将内联函数插入。不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
结构体
结构体是C编程中一种用户自定义的数据类型,类似于Java中的class类型。
//Student 相当于类名
//student和a 可以不定义,表示结构变量,也就Student类型的变量
struct Student
{
char name[50];
int age;
} student,a;
//使用typedef定义
typedef struct{
char name[50];
int age;
} Student;
当结构体需要内存过大,使用动态内存申请。结构体占用字节数和结构体内字段有关,指针占用内存就是4或8字节(32位或64位)。
struct Student *s = (Student*)malloc(sizeof(Student));
memset(s,0,sizeof(Student));
printf("%d\n", s->age);
字节对齐
内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址开始访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。字节对齐的问题主要就是针对结构体。
struct MyStruct1
{
short a;
int b;
short c;
};
struct MyStruct2
{
short a;
short c;
int b;
};
默认对齐:1. 某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍;2. 结构所占用的总字节数是结构中字节数最长的变量的字节数的整数倍。
// short = 2 补 2
// int = 4
// short = 2 补 2
sizeof(MyStruct1) = 12
// 2个short在一起组成一个 4
sizeof(MyStruct2) = 8
指定对齐:#pragma pack(?)
#pragma pack(2) //指定以2字节对齐
struct MyStruct1
{
short a;
int b;
short c;
};
#pragma pack() //取消对齐
//short = 2
//int = 4
//short = 2
合理的利用字节可以有效地节省存储空间,不合理的则会浪费空间、降低效率甚至还会引发错误。(对于部分系统从奇地址访问int、short等数据会导致错误)。
共用体
在相同的内存位置存储不同的数据类型,共用体占用的内存应足够存储共用体中最大的成员。
//占用4字节
union Data
{
int i;
short j;
}
union Data data;
data.i = 1;
//i的数据损坏
data.j = 1.1f;
关于对c语言的语法印象,暂别在这里,下一节是C++的一些家常菜,这种文章适合有过c或c++开发的基础的童鞋阅读,引起他们找到昔日初入该语言时的兴奋,也是帮助他们重温一部分失窃的记忆。