在项目中,我们常把一些短小而又频繁使用的函数写成宏函数。宏函数与前面学的函数是一样的,不同的区别是宏函数是使用宏进行定义的,并且宏函数在调用的时候没有普通函数参数压栈出栈等开销,可以提高程序的效率。宏函数实际上是使用空间换取时间的一种做法。在使用宏函数的时候需要注意一下几点:
下面是关于宏函数使用的一个示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
#define MYADD(x,y) ((x) + (y))
/*
1. 宏函数需要加小括号修饰,保证运算的完整性
2. 通常会将频繁、短小的函数写成宏函数
3. 宏函数会比普通函数在一定程度上效率高,省去普通函数入栈出栈时间上的开销
优点:以空间换时间
*/
int main(int argc, char* argv[])
{
printf("%d\n", MYADD(3, 4) * 2);
system("pause");
return 0;
}
#endif
程序的运行增加栈区和堆区,而函数的调用就是发生在栈上的。栈是一种先进后出(FILO)的数据结构。在经典的操作系统中,栈的生长方向总是向下的,压栈的操作会使栈顶的地址减少,而出栈的操作会使栈顶的地址增大。
栈在程序的运行中是很重要的。栈保存了一个函数调用所需要维护的信息,这个通常被我们称为栈帧。一个函数调用过程所需的信息主要有以下方面:
下面给出函数调用的一个栈区示意图:
对函数的调用时入栈出栈有一定了解之后我们还需要知道函数调用惯例。所谓函数调用惯例就是函数的调用方和被调用方对于函数是如何调用的必须有一个明确的规定,只有双方都遵守同样的约定,函数才能被正确的调用。比如如果函数调用方传递参数的时候先压入a参数,再压入b参数,而被调函数则认为先压入的b参数,后压入的是a参数,那么被调用函数在使用a、b值的时候就会颠倒。一个调用惯例一般包含两方面,一方面是函数参数的传递顺序和方式;另一个方面是栈的维护方式。
函数的传递有很多种方式,最常见的是通过栈进行传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例规定了函数调用方将参数压栈的顺序:从左往右还是从右往左。有些调用惯例还允许使用寄存器传递参数,以此提高程序的性能。
在函数参数入栈,函数体被调用后,还需要将压入的参数全部弹出,使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以是被调用方完成。
为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰。在C语言中有着多种调用惯例,而默认的调用惯例是cdecl
。任何一个没有显示指定调用惯例的函数都是默认cdecl
惯例。
下面给出调用惯例的表格:
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右往左参数入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右往左参数入栈 | 下划线+函数名+@+参数字节数 |
fastcall | 函数本身 | 前两个参数由寄存器传递,其余参数通过堆栈传递 | @+函数名+@+参数的字节数 |
pascal | 函数本身 | 从左往右参数入栈 | 较为复杂,可以查阅相关文档 |
在函数的调用过程中,被调函数在栈上开辟的内存空间主调函数是不能使用的,只有被调函数或者被调函数调用的函数以及更深层次调用的函数才可以使用。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
char *func()
{
char *p = malloc(10); // 堆区数据,只要没有释放都可以使用
int c = 10; // 在func中可以使用,test01和main都不可以使用
return p;
}
void test01()
{
int b = 10; // 在test01和func可以使用,在main中不可以使用
func();
}
int main(int argc, char* argv[])
{
int a = 10; // 在main、test01、func中都可以使用
system("pause");
return 0;
}
#endif
栈的生长方向是由高地址往低地址生长,内存存放的时候则是按照小端对齐的方式进行存放。如定义int a = 0x11223344
,int
占四个字节,此时44
存放在低地址上,11
存放在最高地址上。当然不同的编译器可能栈的生长方向以及内存存放方向可能不一样。上述的内容则是以VS为例。
变量的生长方向和内存存放方向如图所示:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
// 栈的生长方向
void test01()
{
int a = 10; // 栈底 高地址
int b = 20;
int c = 30;
int d = 40; // 栈顶 低地址
printf("%p\n", &a);
printf("%p\n", &b);
printf("%p\n", &c);
printf("%p\n", &d);
}
// 内存存放方向
void test02()
{
int a = 0x11223344;
unsigned char *p = &a;
printf("%x\n", *p); // 低位字节数据 低地址
printf("%x\n", *(p + 1)); // 高位字节数据 高地址
}
int main(int argc, char* argv[])
{
//test01();
test02();
system("pause");
return 0;
}
#endif
指针也是一种数据类型,占用内存空间,用于保存内存地址。在C语言标准中,定义了NULL
指针,这是一个特殊的指针变量,不指向任何内存空间。这个指针我们称为空指针。既然它不指向任何内存空间,所以我们不能对空指针进行解引用操作,如果非要操作,则会出现一些意想不到的错误,结果因编译器而异。
在我们使用指针的时候,特别要避免野指针的出现。野指针是一个指向了没有权限访问内存空间的指针,该空间可能是已经释放的或者其它非法情况的。野指针的出现主要有三个原因,分别为:
而操作野指针是非常危险的。为了避免,我们需要在指针定义的时候就初始化为NULL
,在指针指向的内存释放的时候,我们也需要将指针赋值置为NULL
。当然空指针是可以重复释放的,而野指针是不能重复释放的。通常我们判断一个指针是否合法,都是使用if
语句测试该指针是否为NULL
,但是却判断不出来是否为野指针,因此一定要置空。
关于野指针和空指针的示例代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
// 不能向NULL或者非法内存拷贝数据
void test01()
{
//char *p = NULL;
给p指向的内存区域拷贝数据
//strcpy(p, "1314");
//char *q = 0x11223344;
给q指向的内存区域拷贝内容
//strcpy(q, "1314");
}
// 指针操作超越变量作用域
int *tmpFunc()
{
int a = 10;
int *p = &a;
return p;
}
// 野指针出现情况
void test02()
{
// 指针变量未初始化
/*int *p;
printf("%d\n", *p);*/
// 指针释放后未置空
char *str = malloc(100);
free(str);
// 记住释放后置空,防止野指针出现
//str = NULL;
//free(str);
// 空指针可以重复释放、野指针不可以重复释放
// 指针操作超越变量作用域
int *p = tmpFunc();
printf("%d\n", *p);
printf("%d\n", *p);
}
int main(int argc, char* argv[])
{
//test01();
test02();
system("pause");
return 0;
}
#endif
指针所指向的内存空间决定了指针的步长。指针的步长指的是当指针+1的时候,移动多少个字节单位。当然指针的步长也将决定了解引用的时候解出来的字节数。关于指针步长的示例代码如下所示:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
#include
// 指针的步长代表指针+1之后跳跃的字节数
void test01()
{
char *p = NULL;
printf("%d\n", p);
printf("%d\n", p + 1);
double *p2 = NULL;
printf("%d\n", p2);
printf("%d\n", p2 + 1);
}
// 解引用的时候,解出的字节数量
void test02()
{
char buff[1024] = { 0 };
int a = 1314;
memcpy(buff + 1, &a, sizeof(int));
char *p = buff;
printf("%d\n", *(int *)(p + 1));
}
// 结构体步长
struct Person
{
char *name;
unsigned int age;
char tel[15];
double high;
double weight;
char addr[1024];
};
#define getOffset(s,m) ((unsigned int)&(((s *)0)->m))
void test03()
{
struct Person p = {"哥斯拉", 250, "00100010010", 1314.520, 88888, "火焰山"};
printf("%.lf\n", *(double *)((char *)&p + 32));
printf("%.lf\n", *(double *)((char *)&p + offsetof(struct Person, weight)));
printf("%.lf\n", *(double *)((char *)&p + getOffset(struct Person, weight)));
}
int main(int argc, char* argv[])
{
//test01();
//test02();
test03();
system("pause");
return 0;
}
#endif
在上述代码中我们使用了一个offsetof(s,m)
这个宏函数,求的是结构体中某个成员变量偏移的字节数。其定义的原型为:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
其中是将0
地址强转为对应数据类型指向的地址,然后指向对应的内存空间,将内存空间取地址强转为size_t
类型就得到了偏移量。这里的size_t
实际上就是unsigned int
类型。不过使用该宏函数的前提是引入
头文件。
指针存在的意义主要是为了间接赋值。可以通过指针调用其它函数来改变原函数某些变量的值等。通过指针间接赋值成需要三个条件,分别是:
*
操作指针指向的内存关于指针间接赋值的示例如下所示:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
// 间接赋值的三大条件
// 一个普通变量和指针变量或者一个实参一个形参
// 建立关系
// * 操作内存
void changeValue(int *a)
{
*a = 1000;
}
int main(int argc, char* argv[])
{
int a = 10;
int *p = NULL;
p = &a;
*p = 100;
printf("%d\n", a);
int a2 = 10;
changeValue(&a2);
printf("%d\n", a2);
system("pause");
return 0;
}
#endif
指针做函数参数的时候,具备了输入和输出特性。输入的时候是主调函数分配内存,通过指针去改变主调函数分配的内存中的值;而输出的时候则是主调函数分配内存,而主调函数可以通过函数调用获取到分配的内存以及内存中的内容。示例代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
// 输入特性: 主调函数分配内存,被调函数使用
void func(char *p)
{
strcpy(p, "I love you forever!");
}
void test01()
{
// 在test01中分配了内存,在栈上
char buff[1024] = { 0 };
func(buff);
printf("%s\n", buff);
}
void printString(char *str)
{
printf("%s\n", str);
}
void test02()
{
char *p = malloc(sizeof(char) * 64);
memset(p, 0, sizeof(char)* 64);
strcpy(p, "I love you forever~~~");
printString(p);
if (p != NULL)
{
free(p);
p = NULL;
}
}
// 输出特性: 在被调函数中分配内存,主调函数使用
void allocateSpace(char **pp)
{
char *str = malloc(sizeof(char)* 64);
memset(str, 0, sizeof(char) * 64);
strcpy(str, "I love you whom do not love me!");
*pp = str;
}
void test03()
{
char *p = NULL;
allocateSpace(&p);
printf("%s\n", p);
}
int main(int argc, char* argv[])
{
test01();
test02();
test03();
system("pause");
return 0;
}
#endif
字符串指针的使用是最常用的,下面给出一些关于字符串指针使用的例子:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
void test01()
{
// 字符串结束标志位 \0
char str1[] = { 'h', 'e', 'l', 'l', 'o', '\0' };
printf("%s\n", str1);
char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n", str2);
char str3[] = "hello";
printf("%s\n", str3);
printf("sizeof str3: %u\n", sizeof str3);
printf("strlen str3: %u\n", strlen(str3));
char str4[100] = "hello";
printf("%s\n", str4);
printf("sizeof str4: %u\n", sizeof str4);
printf("strlen str4: %u\n", strlen(str4));
char str5[] = "hello\0world";
printf("%s\n", str5);
printf("sizeof str5: %u\n", sizeof str5);
printf("strlen str5: %u\n", strlen(str5));
char str6[] = "hello\012world";
printf("%s\n", str6);
printf("sizeof str6: %u\n", sizeof str6);
printf("strlen str6: %u\n", strlen(str6));
}
// 字符串拷贝实现
// 利用[]实现
void copyString01(char *dest, char *src)
{
int len = strlen(src);
for (int i = 0; i < len; i++)
{
dest[i] = src[i];
}
dest[len] = '\0';
}
// 利用字符串指针实现
void copyString02(char *dest, char *src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = '\0';
}
void copyString03(char *dest, char *src)
{
while (*dest++ = *src++);
}
void test02()
{
char *str = "I love you forever!";
char buf[1024];
//copyString01(buf, str);
//copyString02(buf, str);
copyString03(buf, str);
printf("%s\n", buf);
}
// 字符串翻转
void reverseString01(char *str)
{
// 利用[]
int len = strlen(str);
int start = 0;
int end = len - 1;
while (start < end)
{
char tmp = str[start];
str[start] = str[end];
str[end] = tmp;
start++;
end--;
}
}
void reverseString02(char *str)
{
int len = strlen(str);
char *start = str;
char *end = str + len - 1;
while (start < end)
{
char tmp = *start;
*start = *end;
*end = tmp;
start++;
end--;
}
}
void test03()
{
char str[] = "hello world";
//reverseString01(str);
reverseString02(str);
printf("%s\n", str);
}
int main(int argc, char* argv[])
{
//test01();
//test02();
test03();
system("pause");
return 0;
}
#endif
上述代码中test01
是了解字符串中sizeof
和strlen
函数使用的区别。而在test02
中则是通过指针以及下标等方法实现了对一个字符串的拷贝。在test03
中实现了对一个字符串的翻转。
在字符串中,常常需要格式化字符串的内容。对于字符串的格式化有sprintf
函数,该函数的原型为int sprintf(char *str, const char *format, ...);
。该函数的功能为根据参数format
字符串来转换并格式化数据,然后将结果输出到str
指定的空间中,直到出现字符串结束符\0
为止。参数str
是字符串的首地址,format
为字符串格式,用法与printf
函数一样。返回值为字符串的长度,若失败则返回-1
。关于sprintf
的示例代码如下所示:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
char buf[1024];
// 格式化字符串
memset(buf, 0, 1024);
sprintf(buf, "I love %s, she is %d year old around", "九天玄女", 99999);
printf("%s\n", buf);
// 拼接字符串
memset(buf, 0, 1024);
char *str1 = "九天玄女";
char *str2 = "我爱你";
sprintf(buf, "%s%s", str1, str2);
printf("%s\n", buf);
// 数字转字符串
memset(buf, 0, 1024);
int num = 520;
sprintf(buf, "%d", num);
printf("%s\n", buf);
// 设置宽度右对齐
memset(buf, 0, 1024);
sprintf(buf, "%10d", num);
printf("%s\n", buf);
// 设置宽度左对齐
memset(buf, 0, 1024);
sprintf(buf, "%-10d", num);
printf("%s|\n", buf);
// 转成16进制字符串
memset(buf, 0, 1024);
sprintf(buf, "0x%x", num);
printf("%s\n", buf);
// 转成8进制字符串
memset(buf, 0, 1024);
sprintf(buf, "0%o", num);
printf("%s\n", buf);
system("pause");
return 0;
}
#endif