c语言总结(二)

c语言总结(二)

函数调用流程

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用到了栈,没有栈就没有函数,没有局部变量,也就没有如今我们所见的所有的计算机的语言。
在经典计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(push),也可以将压入栈中的数据弹出(pop),但是在栈容器当中必须遵循一条规则:先入栈的数据最后出栈。
在经典计算机的操作系统当中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。栈保存一个函数调用所需维护的信息,这通常被称为堆栈帧(stack Frame)或者活动记录(Activate Record),一个函数调用的过程所需要的信息一般包括以下几个方面:

  • 函数的返回地址
  • 函数的参数
  • 临时变量
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

函数调用方和被调用方对于函数如何调用必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为“调用惯例”,一个调用惯例一般都包含以下几个方面:

  • 函数参数的传递方式
    函数的传递有很多种方式,最常见的是通过栈传递。函数的调用将参数压入栈中,函数自己再从栈中将参数取出。对于多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从右向左,还是从左向右。有些调用还允许寄存器传递参数,以提高性能
  • 栈的维护方式
    在函数将参数压入栈中之后,函数体会被调用,此后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个调用的工作可以由函数的调用方来完成,也可以由函数本身来完成。

为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。
事实上,在C语言里,存在着多个调用惯例,而默认的是cdecl。任何一个没有显示指定调用惯例的函数都是默认的cdecl惯例。比如我们上面对于func函数的声明,它的完整的写法是:int _cdecl func(int a, int b);
注意:_cdecl不是标准的关键字,在不同的编译器可能有不同的写法。

调用惯例出栈方参数传递名字修饰
cdecl函数调用方从右向左参数入栈下划线+函数名
stdcall函数本身从右向左参数入栈下划线+函数名+@+参数字节数
fastcall函数本身前两个参数由寄存器传递,其余参数通过堆栈传递。@+函数名+@+参数的字节数
pascall函数本身从左至右参数入栈较为复杂,参考相关文档

栈的生长的方向和内存的存放方向

在常见的X86中内存中的栈的增长的方向就是从高地址向地址增长。
内存的存放方向中存在着大端模式和小端模式
大端(big-Endian)和小端(Little-Endian)的定义:

  • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址段。
  • big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址段。

例如,存放数字0x12345678
(1)大端模式
低地址 ————-> 高地址
0x12 | 0x34 | 0x56 | 0x78
(2)小段模式
低地址 ————-> 高地址
0x78| 0x56 | 0x34 | 0x12
再举两个具体的例子
(1)16bit宽的数0x1234在Little-endian模式和Big-EndianCpu内存中存放的方式(假设地址从0x8000开始存放)

内存地址小端模式存放内容大端存放内容
0x80000x340x12
0x80010x120x34

(2)32bit宽的数0x12345678在Little—Endian模式和Big-Endian模式Cpu内存中存放方式(假设地址从0x8000开始存放)

内存地址小端模式存放内容大端存放内容
0x80000x780x12
0x80010x560x34
0x80020x340x56
0x80030x120x78

大端模式和小端模式各有特点

  • 小端模式:强制转换数据不需要调整字节内容,1、2、4字节的存储方式都一样
  • 大端模式:符号位的判定固定为第一个字节,容易判断正负。

大端模式和小端模式产生的原因
由于CPU的不同而产生的不同模式。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

判断机器是大端模式还是小端模式方法
使用char指针指向int型的数据,看int的首地址存储的内容

int a = 0x1234;
char b = *(char *)&a;
if(b == 0x12)
{
    printf("it is Big-Endion");
}
else
    printf("it is Little—Endion");

在网络通信的时候特别注意大端模式和小端模式的问题,其中可能涉及到转换的问题。

指针

指针是一种数据类型,占用内存空间,用来保存内存地址。
空指针
空指针为指向NULL的指针,它表示不指向任何东西,但是对空指针解引用是一个非法的操作。
野指针
野指针为一个指向已删除的对象或未申请访问受限内存区域的指针。对野指针进行操作很容易造成程序错误。
以下原因会造成野指针:

  • (1)指针变量未初始化:任何指针变量被创建时不会自动成为NULL指针,他的缺省值是随机的,所以,指针变量在创建的同时应该被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
  • (2)指针释放后未置空:有事指针在free或delete后未复制NULL,便会使人以为是合法的,但是他们只是把指针所指的内存给释放掉,但并没有把指针本身干掉,此时指针指向的就是垃圾内存,释放后的指针应立即将指针置为NULL,
  • (3)指针操作超越变量作用域:不要返回指向栈内存的指针或引用,因为栈内存结束时会被释放。

间接访问操作符
通过一个指针访问它所指向的地址的过程叫做间接访问,也被称为解引用指针,用于解引用的操作符为*。
在指针声明的时候,*号表示所声明的变量为指针
在指针使用的时候,*号表示操作指针所指向的内存的指向空间。

  • (1)*是通过地址(指针变量的值)找到指针指向的内存,再操作内存
  • (2)*放在等号的左边为赋值(给内存赋值,写内存):int *p = 10;
  • (3)*放在等号的右边取值(从内存中取值,读内存): int a = *p;

指针的步长
指针是一种数据类型,是指它指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。如:int *p 的步长为4字节,char *p的步长为1字节。
指针的间接赋值的三大条件

  • (1)2个变量(一个指针变量一个普通变量、或者一个实参一个形参):int a = 100; int *p = NULL;
  • (2)建立关系 p = &a;
  • (3)通过*操作指针指向的内存 *p = 20;

实参全称为“实际参数”是在调用时传递给函数的参数,实参可以是常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。
形参全称为“形式参数”由于它不具有实际存在变量,所以又称为虚拟变量,是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。在调用函数时,实参将赋值给形参。因而,必须注意实参的个数,类型应与形参一一对应,并且实参必须有确定的值。
间接赋值的推论

  • 用1级指针形参,去间接修改了0级指针(实参)的值。
  • 用2级指针形参,去间接修改了1级指针(实参)的值。
  • 用3级指针形参,去间接修改了2级指针(实参)的值。
  • 用n级指针形参,去间接修改了n-1级指针(实参)的值。

指针做函数参数
指针做函数参数,具备输入和输出特性:
输入:主调函数分配内存,释放内存的时候,主调函数实现
输出:被调用函数分配内存,释放内存的时候,主调函数使用传出的地址来实现

字符串指针强化

字符串是以“\0”结尾的字符数组
使用 char str1[] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’};初始化时,当输出的时候将从开始位置一直找到0结束的时候,而不一定是o为结尾。
使用 char str2[] = “hello”;初始化时,编译器会默认在字符串尾部添加’\0’.
使用sizeof计算上面数组大小的时候,数组长度包含’\0’。
使用strlen计算字符串的长度,到’\0’为结尾,不包含’\0’。
字符串的拷贝

int mystrcpy(char *str1, char *str2)
{
    if(NULL == str1)
    {
        retrun -1;
    }
    else if(NULL == str2)
    {
        return -2;
    }

    for(int i = 0; str2[i]! = '\0'; ++i)
    {
        str1[i] = str2[i];
    }

    return 0;
}

字符串的反转

void str_reverse(char *str)
{
    if(NULL == str) 
    {
        return;
    }

    int begin = 0;
    int end = strlen(str) - 1;

    while(begin < end)
    {
        char temp = str[begin];
        str[begin] = str[end];
        str[end] = temp;

        ++begin;
        --end;
    }
}

字符串的格式化
sprintf函数

#include<stdio.h>
int sprintf(char *str, const char *format, ...);
//根据参数format的字符串的格式来转换格式化数据,然后将结果输出到str指定的空间中,知道字符串结束符'\0'为止。
参数:
str:字符串地址
format:字符串格式,和printf一样
返回值:
成功:实际格式化的字符个数
失败:-1

sscanf函数

#include<stdio.h>
int sscanf(const char *str, const char *format, ...);
//根据参数的format的字符串读取数据,并根据参数format字符串并转换格式化数据。
格式作用
%*s或%*d跳过数据
%[width]s读取指定宽度的数据
%[a-z]匹配到a到z中的任意字符(尽可能多的匹配)
%[aBc]匹配到a、B、c中一员,贪婪性
%[^a]匹配非啊的任意字符,贪婪性
%[^a-z]表示读取除a-z以外的所有的字符

一级指针易错点

  • 越界
  • 指针叠加会不断的改变指针的方向,导致释放堆空间的时候出现错误
  • 返回局部变量的地址,因为局部地址变量会随局部函数的结束而被释放,这样指针指向的地址就是被释放的地址,是一个不合法的地址。
  • 同一块内存释放多次

const
const可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
在c语言中的const修饰的变量只是一个只读变量,不是常量,可以通过指针间接修改。但C++中不能被修改。
在const修饰指针时:

  • const放在*左侧时,指针指向的空间不能被修改,但是指针的指向可以修改
    如 const int *ptr = 100;
  • const放在*右侧时,指针指向的空间可以修改,但是指针的只想不能被修改
    如 int * const ptr = 100;
  • 当const 出现在*的两侧时,两者都不能被修改。
    如 const int * const ptr = 100;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值