一、宏函数
1.避免了函数的入栈出栈跳转等开销,可以提高程序运行效率。
2.但是多次调用会使代码变得庞大。
宏的有效范围:
从宏定义的位置开始到文件结束或者取消宏定义
1、不做作用域检测
2、不做类型检测
#define SUM(a, b) ((a)+(b))
#define MIN(a, b) ((a)<(b) ? (a) : (b))
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是:如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式种的#else可以没有,既可以写为:
#ifdef 标识符
程序段
#endif
#ifndef 标识符
程序段1
#else
程序段2
#endif
与上一种形式的区别是ifdef改为ifndef。它的功能是:如果标识符未被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。这与第1种形式的功能正好相反
二、#与##
#用于在与编译期间将宏参数转化为字符串
#include <stdio.h>
#define SQR(x) printf (“The square of “#x” is %d \n”, ((x)*(x)))
int main()
{
SQR(8);
return 0;
}
##用于在预编译期间粘连两个符号
#include <stdio.h>
#define CREATEFUN(name1, name2)
void name1##name2()
{
printf ("%s called\n", FUNCTION);
}
CREATEFUN(my, Function);
int main()
{
myFunction();
return 0;
}
三、关键字:
1.static 静态变量在外部 所以函数无法释放它
1、修饰局部变量:可以延长局部变量的生命周期,直到程序结束才会被释放
2、修饰全局变量:只能在本文件使用,其他文件不可用
3、修饰函数:只能在本文件使用,其他文件不可用
// 变量声明:
// g_a这个变量可能存在其他文件中,如果用的时候本文件没有找到,去其他文件找
extern int g_a;
void func()
{
static int a = 10;
a++;
printf (“a = %d\n”, a);
}
int main()
{
int i;
for (i = 0; i < 10; i++)
{
func();
}
// printf (“g_a = %d\n”, g_a);
// printf (“1+2 = %d\n”, add(1,2));
return 0;
}
2.const
const用来定义只读变量,具有不可变性。
1.const修饰一般变量:
一般变量是指简单类型的只读变量。这种只读变量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如:
int const i = 2; 或 const int i = 2;
2.const修饰数组:
定义或说明一个只读数组可采用如下格式:
int const a[5] = {1,2,3,4,5}; 或 const int a[5] = {1,2,3,4,5}
3.const修饰指针:
const int * p; // p可变,p指向的对象不可变
int const * p; // p可变,p指向的对象不可变
int * const p; // p不可变,p指向的对象可变
const int * const p; // 指针p和p指向的对象都不可变
这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近,“近水楼台先得月”,离谁近就修饰谁。
const (int) p //const 修饰p,p是指针,p是指针指向的对象,不可变。
(int) const * p; //const 修饰p,p是指针,p是指针指向的对象,不可变。
( int) * const p;//const 修饰p,p不可变,p指向的对象可变
const ( int) const p; // 前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
4.const修饰函数的参数
const修饰也可以修饰函数的参数,当不希望这个参数值在函数体内被意外改变时使用,例如:
void Fun(const int p);
告诉编译器p在函数体中不能改变,从而防止了使用者的一些无意的或错误的修改。
5.const修饰函数的返回值
const修饰符也可以修饰函数的返回值,返回值不可被改变。例如: const int Fun(void);
3.typedef
使用关键字 typedef 可以为类型起一个新的别名,语法格式为:
typedef oldName newName;
oldName 是类型原来的名字,newName 是类型新的名字。
需要强调的是,typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。
1.给普通类型起别名:
typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;
INTEGER a, b;等效于int a, b;
2.给数组类型起别名:
typedef char ARRAY20[20];
表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:
ARRAY20 a1, a2, s1, s2;
它等价于:
char a1[20], a2[20], s1[20], s2[20];
3.给结构体类型定义别名:
typedef struct stu{
char name[20];
int age;
char sex;
} STU;
STU 是 struct stu 的别名,可以用 STU 定义结构体变量:
STU body1,body2;
它等价于:
struct stu body1, body2;
4.给指针类型定义别名:
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:
PTR_TO_ARR p1, p2;
按照类似的写法,还可以为函数指针类型定义别名:
typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;
5.函数指针:
typedef struct Student
{
int id;
char name[20];
}STU;
int add(int a,int b)
{
return a+b;
}
typedef (*PFUNC)(int a,int b);
int main()
{
STU s1;
s1.id = 20;
PFUNC p1 = add;
printf("%d %d\n",s1.id,p1(4,2));
return 0;
}
typedef与define的区别:
typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。
- 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:
#define INTERGE int
unsigned INTERGE n; //没问题
typedef int INTERGE;
unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
- 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int *
PTR_INT p1, p2;
经过宏替换以后,第二行变为:
int *p1, p2;
这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。
相反,在下面的代码中:
typedef int * PTR_INT
PTR_INT p1, p2;
p1、p2 类型相同,它们都是指向 int 类型的指针。
四、内存管理
栈又叫堆栈,存的是所有的自动变量(局部变量)、函数形参,这个是由系统进行自动完成,不需要程序员自己考虑存放。栈它的操作方式类型与数据结构中的栈,都是先进后出的操作。当一个函数被调用的时候,这个函数的返回地址和一些其他的调用信息,都会被存放到栈区。然后这被调用的函数再为它的自动变量(局部变量)还有一些形参在栈区分配内存空间,这就是C实现递归调用的方法。每执行一次递归调用,一个新的栈框架就会被使用,这样做这个新的栈框架里的变量就不会和该函数的另一个实例栈框架里面的变量搞混。
堆: 这个区是由程序员自己管理的,在程序运行过程中进行动态分配的内存。你可以用malloc()系列函数进行动态的添加和释放。堆的大小并不是固定的,可以动态的扩张或者收缩。
堆栈存放的是局部变量,是动态分配的。
数据区和代码段存放全局变量、静态变量,是静态变量。
五、内存管理函数
1.malloc:
malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。
由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源,所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。
这两个函数的库头文件为:
#include <stdlib.h>
函数原型:
void *malloc(size_t size)
2.free()函数释放原先申请的内存空间。
void free(void *ptr);
3.memset:
void *memset(void *s, int c, size_t n);
将s中当前位置后面的n个字节 用 c 替换并返回 s
#include <stdio.h>
#include <stdlib.h>
int main1()//分配二维数组 int a[3][4]
{
int (*pa)[4] = (int(*)[4])malloc(sizeof(int)/sizeof(char)*12);
int i ;
pa[1][2] = 10;
printf("%d\n",pa[1][2]);
free(pa);
return 0;
}
int main()//分配不连续空间
{
int **p = (int **)malloc(sizeof(int *)/sizeof(char)*3);
int i,j;
for(i = 0;i<3;i++)
{
p[i] = (int *)malloc(sizeof(int)/sizeof(char)*4);
}
*p[6]=10;
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
{
p[i][j]+=2;
}
}
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
{
printf("p[%d][%d] = %d\n",i,j,p[i][j]);
}
}
for(i = 0;i < 3;i++)
{
free(p[i]);
}
free(p);
return 0;
}
六、堆和栈的区别
1.管理方式不同:
栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。
2.空间大小不同:
栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。
堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。
3.是否产生碎片:
对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。
4.增长方向不同:
堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。
5.分配方式不同:
堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。
6.分配效率不同:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。