1.函数调用模型
1.1 函数调用流程
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),
也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后
出栈(先进后出).
在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。
栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面:
函数的返回地址;
函数的参数;
临时变量;
保存的上下文:包括在函数调用前后需要保持不变的寄存器。
**宏函数不是函数,宏函数在一定场景下效率更高,对于频繁使用的,并且代码量短小的
函数 因为宏函数没有与普通函数调用的开销(函数压栈,跳转,返回等),因此使用宏函数。**
1.2 调用惯例
一个调用惯例一般包含以下几个方面:
函数参数的传递顺序和方式
1.函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方先将将参数压入栈中,
函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压
栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。在C语言中,对于子函数的调用中,一般情况下都是(假设都是在栈区存储的变量)
(1):先将子函数的形参压入栈中;
(2):然后将传入的实参的值放入到形参中
(3):然后跳转到返回地址,也就是在主函数中调用子函数的代码的下一行代码的地址;
(4):然后再执行子函数,将子函数中内部定义的局部变量压入栈中,局部变量的值如果
和子函数的形参有关,就把形参的值赋与局部变量,如果没有使用形参的值,就直接在内
存中存储定义局部变量时赋予的值。
(5):若需要返回值,且返回值较小的时候,则将运算的运算结果存储与寄存器中,将寄
存器的值赋予主函数中定义的用来存储返回值的变量,否则将内容存储在栈中,将栈中的内
容赋予主函数中接收的变量;
(6):函数调用过程结束,释放栈区空间
2.栈的维护方式
为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同
的调用惯例有不同的名字修饰策略。
在c语言里,存在着多个调用惯例,而默认的是cdecl(由函数调用方释放内存,参数
由右向左进行入栈).任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如
我们上面对于func函数的声明,它的完整写法应该是:
int _cdecl func(int a,int b);
注意: _cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就
不存在_cdecl这样的关键字,而是使用__attribute__((cdecl)).
1.3 栈的生长方向和内存存放方向
1.栈的生长方向:自顶向下生长,由高地址(栈顶)向低地址(栈底)生长
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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);
}
int main()
{
test01();
return 0;
}
2.单个数据在内存中的存储,比如0xaabbccdd,在内存中的存储顺序应该为ddccbbaa,
即高位字节存放的是数据的高位地址(小端模式)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test()
{
int a = 0xaabbccdd;
unsigned char* p = &a;
printf("%x\n", *p);
}
int main()
{
test();
return 0;
}
2.指针
2.1 空指针和野指针
(1):空指针
NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可
以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未执行任何东西,
因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NUL针。
(2):野指针
在使用指针时,要避免野指针的出现:
导致野指针的三种抢矿:
1.指针变量未初始化(牢记指针必须初始化)
指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内
存。
2.指针释放后未置空(使用指针后记得置NULL)
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名
字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此
时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
3.指针操作超越变量作用域(不要返回局部变量的地址)
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
2.2 指针的步长
指针的步长指的是,当指针+1时候,移动1个指针类型字节大小的距离。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stddef.h>
//指针步长:指针变量+1 要想后跳(指针类型大小)的字节
//指针的类型:不单单决定指针的步长,还决定解引用的时候从给定地址开始取类型大小的字节数
void test01()
{
//步长
char* p = NULL;
printf("%d\n", p);
printf("%d\n", p + 1);
printf("-----------------\n");
int* p2 = NULL;
printf("%d\n", p2);
printf("%d\n", p2 + 1);
printf("-----------------\n");
char buffer[1024] = { 0 };
int a = 100;
memcpy(buffer + 1, &a, sizeof(int));
char* p3 = buffer;
//从p3+1的位置开始找,找连续的四个字节(int),然后取出结果
printf("*p3[1]---*p3[4] = %d\n", *(int*)(p3 + 1));
}
struct Person
{
int a;
char b;
char buf[64];
int d;
};
void test02()
{
struct Person p1 = { 10,'a',"hello python!",20 };
char b;
//offsetof是计算偏移量的宏函数,定义在stddef.h头文件中,即计算b距离Person类型的首地址的偏移量
printf("结构体中b的偏移量为:%d\n", offsetof(struct Person,b));
printf("d = %d\n",*(int* )((char* )&p1 + offsetof(struct Person, d)));
}
int main()
{
test01();
test02();
return 0;
}
2.3 指针作函数参数
指针做函数参数,具备输入和输出特性:
输入:主调函数分配内存,被调函数使用内存
输出:被调函数分配内存,主调函数释放内存
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void printString(const char* arr)
{
printf("%s\n",arr);
}
void printfArrString(const char** arr,int len)
{
for (int i = 0; i < len; i++)
{
printf("%s\n",arr[i]);
}
}
//1.主调函数分配内存,被调函数使用内存 指针的输入特性
void test01()
{
//堆上分配内存
char* s = malloc(sizeof(char) * 100);
memset(s, 0, 100);
strcpy(s, "hello world");
printString(s);
free(s);
//栈上分配内存
char* str[] = { "aaaaaa","bbbbbb","cccccc","dddddd","eeeeee", };
int strLen = sizeof(str) / sizeof(str[0]);
//printf("%d\n", strLen);
printfArrString(str, strLen);
}
//2.输出特性 被调函数分配内存,主调函数使用内存
void allocateSpace(char** temp)
{
char* p = malloc(100);
memset(p, 0, 100);
strcpy(p, "hello world");
*temp = p;
}
void test02()
{
char* p = NULL;
allocateSpace(&p);
printf("%s\n", p);
if (p != NULL)
{
free(p);
p = NULL;
}
}
int main()
{
//test01();
test02();
return 0;
}
2.4 指针函数和函数指针的区别(转)
2.4.1 指针函数,就不多做详解了,即函数的返回值为一个指针。
指针函数的写法:
int *fun(int x,int y);
int * fun(int x,int y);
int* fun(int x,int y);
注:在调用指针函数时,需要一个同类型的指针来接收其函数的返回值。
不过也可以将其返回值定义为 void*类型,在调用的时候强制转换返回值为
自己想要的类型来接受。
2.4.2.函数指针
2.4.2.1 函数指针定义
函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,
函数指针就是指向函数的指针。
格式:类型说明符 (*函数名) (参数)
示例:
#include<iostream>
using namespace std;
int add(int x, int y) {
return x + y;
}
int sub(int x, int y) {
return x - y;
}
//函数指针
int (*fun)(int x, int y);
int main(int argc, char* argv[])
{
//QApplication a(argc, argv);
//第一种写法
fun = add;
cout << "(*fun)(1,2) = " << (*fun)(1, 2) << endl;;
//第二种写法
fun = ⊂
cout << "(*fun)(5,3) = " << (*fun)(5, 3) <<"\t"<< fun(5, 3)<<endl;
system("pause");
return 0;
}
2.4.3 函数指针和指针函数的区别:
1.指针函数本质是一个函数,其返回值为指针;
函数指针本质是一个指针,其指向一个函数。
2.指针函数:int* fun(int x,int y);
函数指针:int (*fun)(int x,int y);
可以理解为,指针函数的*是属于数据类型的,而函数指针的星号是
属于函数名的。
再简单一点,可以这样辨别两者:函数名带括号的就是函数指针,
否则就是指针函数。
总结:指针函数就是一个返回值为指针的函数,函数指针是一个指向
函数的指针。一个很简单的判定方法就是观察(*),比如 int (*p)(int,int);
有括号,*与p结合,*p就是个指针,
** 指向返回值为整型且有两个整型参数的函数 **。
int*p(int,int); 没有括号,*与int结合,int*为返回类型,p就是一
个函数名,这时就是一个指针函数,同样有两个int形参,只不过返回值
类型为int*。
(转自详解指针函数和函数指针)
3.字符串
3.1字符串格式化
1.sprintf函数
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
功能:
根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,
直到出现字符串结束符 '\0' 为止。
参数:
str:字符串首地址
format:字符串格式,用法和printf()一样
返回值:
成功:实际格式化的字符个数
失败: - 1
2.sscanf函数
#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
功能:
从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:
str:指定的字符串首地址
format:字符串格式,用法和scanf()一样
返回值:
成功:实际读取的字符个数
失败: - 1
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void test01()
{
char ch1[] = "helloworld@qq.conm";
char buffer1[1024] = { 0 };
sscanf(ch1, "%[^@]", buffer1); //截取到helloworld
//sscanf(ch1, "%s@%s", buffer1,buffer2);
printf("%s\n", buffer1);
sscanf(ch1, "%*[^@]@%s", buffer1);//任意一个非@符号的字符都不截取,然后匹配到@后截取所有字符串
printf("%s\n", buffer1);
}
int main()
{
test01();
return 0;
}