标准c语言11

一、程序的内存分段:

当执行程序的运行命令后,操作系统会给程序分配它所需要的内存,并划分成以下内存段供程序使用:

text 代码段:

C代码被翻译成二进制指令后存储在可执行文件中,当可执行文件被操作系统执行时,它会把里面的二进制指令(编译后的代码)加载到这个内存段,它里面的内容决定了程序如何执行,为了避免程序被破坏、修改,所以它的权限是只读。

该内存段分为两个部分:

r-x:二进制指令 r--:常量数据

int num = 10;
printf("Hello World\n")

注意:该内存段的内容如果被强制修改会产生段错误(非法使用内存)。【char* p = “heheheh”】

data 数据段:

存储的是初始化过的全局变量(初始化的值不为0)

存储初始化过的静态局部变量(被static修饰过的局部、块变量)

存储在内存段的变量,被const修饰后,就会改存储到text内存段,变成真正的常量。

bss 静态数据段:

存储的是未初始化的全局变量

存储未初始化过的静态局部变量(被static修饰过的局部、块变量)

操作系统把程序被加载到内存后,会把该内存段进行初始化,也就是所有字节赋值为零,所以全局变量的默认值不是随机,而是零。

heap 堆:

该内存段由程序员手动调用内存管理函数(malloc/free),进行分配、释放,它的分配释放受程序员的控制,适合存储一些需要长期使用的数据。

它的大小不受限制,理论上能达到物理的上限,所以适合存储大量的数据。

该内存段无法取名字,也就是无法与标识符建立联系,必须与指针配合使用,使用麻烦。

stack 栈:

存储的是局部变量、块变量

该内存段会随着程序的执行自动的分配(定义局部变量、块变量)、释放(函数执行完毕自动释放局部变量、块变量),虽然使用比较方便,但它的释放不受程序控制,长期使用的数据不能存储在栈内存中。

该内存的大小有限,在终端执行: ulimit -s 可以查看当前系统栈内存的使用上限,我们使用虚拟机ubuntu的栈内存使用上限是8192kb,一旦超过这个限制就会产生段错误。可以使用ulimit -s <size> 命令设置栈内存的使用上限。

静态内存:

当程序完成编译 text、data、bss 三个内存段的大小就确定,在程序运行期间大小不会有任何变化,可以使用size命令查看程序的这三个内存段的大小。

sunll@:~/标准C语言$ size ./a.out
   text    data     bss     dec     hex filename
   3884     312      96    4292    10c4 ./a.out
动态内存:

heap、stack两个内存段,会随着程序的执行,而动态变化。

当程序运行时,/proc/程序编号/maps 文件里记录程序执行过程中内存的使用情况,程序运行结束这个文件就消失了。

使用ps aux 命令查看所有进程的编号,getpid函数可以获取当前进程的编号。

二、变量属性和分类

变量的属性
  • 作用域:变量的使用范围。

  • 存储位置:变量使用那个内存段存储数据,决定了变量在运行期间能否被释放(销毁),能否被修改。

  • 生命周期:变量从定义、分内存到内存销毁的时间段。

全局变量:

定义在函数外的变量叫全局变量。

  • 作用域:本程序中任何位置都可以使用。

  • 存储位置:初始化的全局变量使用的是data内存段,未初始化的全局变量使用的是bss内存段。

  • 生命周期:从程序开始执行,到程序执行结束。

局部变量:

定义在函数内的变量叫局部变量。

  • 作用域:只能在它所在的函数内使用(从定义的位置开始,到函数结束)。

  • 存储位置:使用的是stack内存段。

  • 生命周期:当它所在的函数被调用后,执行到局部变量的定义语句时局部变量就会被创建(操作系统会给局部变量的变量名分配一块stack内存),当函数执行结束后,局部变量就被销毁了。

块变量:

定义在if、for、while、do while语句块内的变量叫块变量,就是特殊的局部变量。

  • 作用域:只能在它所在的语句块内使用。

  • 存储位置:使用的是stack内存段。

  • 生命周期:当它所在的函数被调用后,执行到块变量的定义语句时块变量就会被创建(操作系统会给块变量的变量名分配一块stack内存),当出了它所在的大括号,块变量就被销毁了。

#include <stdio.h>
​
int main(int argc,const char* argv[])
{
    for(int i=0; i<10; i++)
    {
        printf("%p\n",&i);
    }
    for(int j=0; j<10; j++)
    {
        printf("%p\n",&j);
    }
}
// 地址相同,说明当变量i出for循环就已经被销毁,之前属于它的内存被重新分给了变量j。

注意:全局变量、局部变量、块变量可以同名,不会造成命名冲突,局部变量会屏蔽同名的全局变量,块变量会屏蔽同名的全局变量、局部变量。

#include <stdio.h>
​
int num = 123;
int main(int argc,const char* argv[])
{
    // 此时使用的是全局变量
    printf("%d\n",num);
​
    int num = 456;
    // 此时使用的是局部变量,同名的全局变量已经被屏蔽
    printf("%d\n",num);
    
    do{
        printf("%d\n",num);
        int num = 789;
        // 此时使用的是块变量,同名的全局变量、局部变量已经被屏蔽
        printf("%d\n",num);
    }while(0);
}

全局变量的优点和缺点:
优点:

使用方便,避免了函数之间传参产生的消耗,提高程序的运行速度。

缺点:

程序运行期间全局变量所占用的内存不会被销毁,可能会产生内存浪费。

命名冲突的可能性比较大,可能会与其它文件的全局变量、函数、结构、联合、枚举、宏命名冲突。

总结:

全局变量尽量少用,或者不用。

建议:全局变量首字母大写,局部变量全部小写

三、修饰变量的关键字

<类型限定符> 数据类型 变量名;

typedef

变量被typedef修饰后,就会变成定义它的数据类型,之后就可以使用这种新的数据类型定义变量、数组了,该功能是为了给复杂的数据类型重新定义一个简短的类型名。

由于无符号整型使用比较麻烦,所以标准库中为我们定义一些简短的无符号整型的类型名,就使用typedef定义的,实现在stdint.h头文件里。

typedef signed char     int8_t;
typedef short int       int16_t;
typedef int             int32_t;
typedef long long int   int64_t;
​
typedef unsigned char       uint8_t;
typedef unsigned short int  uint16_t;
typedef unsigned int        uint32_t;
typedef unsigned long long int  uint64_t;

注意:在之后的学习过程中,如果遇到一些xxx_t的数据类型,都使用typedef重定义,例如:time_t,size_t。

auto

int num;

早期的C语言用它来修饰自动分配、释放内存的变量,也就是局部变量和块变量,但由于代码使用的变量绝大多数都是局部变量和块变量,所以就约定,该关键字不加就代码加,所以该关键字已经没有实用价值了。

在C++11的语法标准中,auto有了新的功能,就是定义自动类型的变量,编译器会根据变量的初始值,自动设置变量的数据类型。 ​ auto num = 1234;

编译指令:g++ xxx.c -std=c++11

注意:虽然auto关键字,已经不再使用,但基本功能还保留着,所以它不能修饰全局变量。

const

const的意思是常量,但实际它只是为变量提供一层保护,被它修饰的变量不能显式修改,但可以隐式修改,也就被它修饰后并不能变成真正的常量。

#include <stdio.h>
​
int main()
{
    const int num = 1234;
​
    int* p = (int*)&num;
    *p = 6666;
​
    printf("%d\n",num);                                                         
}
// 执行结果是:6666

注意:存储在data内存段的变量,被const修饰后就会变成真正的常量,存储位置被修改为text,其实是修改了data段和text段的分界线。

#include <stdio.h>
​
const int num = 1234;                                                           
int main()
{
​
    int* p = (int*)&num;
    *p = 6666;
​
    printf("%d\n",num);
}
// 执行后会出现段错误
static

static既可以修饰变量,也可以修饰函数,主要有三大功能:

限制作用域:

默认情况下全局变量、函数的作用域是整个程序都可以使用,被static修饰后,就只能在它所在的.c文件内使用。

该功能可以避免全局变量、函数的命令冲突,也能防止全局变量、函数被外部修改、调用,提高代码的安全性。

普通全局变量、函数也叫外部变量、外部函数,被static修饰后就叫做内部变量、内部函数、静态全局变量。

改存储位置:

局部变量、块变量被static修饰后,存储位置就由stack改data、bss,称呼为静态局部变量、静态块变量。

静态局部变量、静态块变量的默认值不再是随机的,而是零。

延长生命周期:

由于静态局部变量、静态块变量的存储位置由stack(动态分配、释放)改为data、bss,所以静态局部变量、静态块变量不会随着函数的执行结束而销毁,而是和全局变量的生成周期一样。

#include <stdio.h>
​
void func(int i)
{
    static int num = 1;
    num *= i;
    printf("%d\n",num);
}
​
int main()
{
    for(int i=1; i<10; i++)
    {   
        func(i);
    }   
}
注意:

static修饰局部变量、块变量,会改变它们的存储、延长生命周期,但并不会改变它们的作用域。

volatile
int num = 123;
...
if(num == num)
{
        
}
// 默认情况下,比较结果永远为真
​
​
volatile int num = 123;
...
if(num == num)
{
        
}
// num被volatile修饰后,比较结果可能不为真

在程序中使用到num变量时,系统会从内存中读取该num的值交给CPU运算,如果之后num变量的值没有发生明显变化,再次使用变量时系统会直接使用上次读取的旧值,而不会再从内存中读取。这编译器对变量读值过程的优化。

volatile 关键字就告诉编译器不要优化变量的读值过程,每使用该变量时,都重新从内存中读取它的值。

什么情况下需要使用volatile关键字:

变量被共享访问,且有多个执行者可以修改它的值,这种情况下变量就应该被volatile修饰。

情况1:多线程编程处理复杂问题时。

情况2:裸机编程、驱动编程时,软硬件共用的寄存器。

register

计算机的存储介质读写速度排序:机械硬盘->固态硬盘->内存条->高级缓存->CPU寄存器

register关键字的作用是申请把变量的存储介质由内存条改为CPU寄存器,一旦申请成功,变量的读写速度、运算速度会大大提高。

#include <stdio.h>                                                              
int main()
{
    int index = 0;
    while(index < 0x7fffffff)
        index++;
}
​
/*
real    0m0.463s
user    0m0.463s
sys 0m0.001s
*/
​
​
#include <stdio.h>                                                              
int main()
{
    register int index = 0;
    while(index < 0x7fffffff)
        index++;
}
/*
real    0m0.463s
user    0m0.463s
sys     0m0.001s
*/

注意:CPU中的寄存器数量有限,申请不一定成功,只有需要长期大量运算的变量才适合用register关键字修饰。

注意:被register修饰过的变量,不能获取变量的地址。

extern

当使用其它.c文件中的全局变量时,需要像声明函数一样,对其它.c文件全局变量进行声明。

extern 类型 变量名; 

注意:声明变量只能解决编译时的问题,如果目标文件最终链接时,变量没有定义,依然会报错。

a.c:(.text+0x12):对‘num’未定义的引用,这种是链接时的错误。
计算机的内存长什么样子:

1、计算机的内存就像是一叠非常厚的"便签",一张便签就相当于一个字节的内存,一个字节有8个二进制位。

2、每一张"便签"的都有自然排序形成的一个编号,计算机根据便签的编号访问、使用"便签"。

3、CPU会有若干个金手指,每根金手指能感知高低两种电流,低电流当作二进制的0,高电流当作二进制的1,我们所说的32位的CPU,指的是CPU有32个金手指用于感知便签的编号:

便签的最小编号 00000000000000000000000000000000 = 0
便签的最大编号 11111111111111111111111111111111 = 4294967295
所以32位的CPU最多能使用 4294967296byte->4194304kb->4096mb->4gb

4、便签的编号也就是内存的地址,是一种无符号整数。

什么是指针:

1、指针(pointer)是一种特殊的数据类型,使用它可以定义指针变量,简称指针。

2、指针变量中存储的是内存的地址,是一种无符号的整数。

3、通过指针变量中记录的内存地址,我们可以读取内存中所存储的数据,也可以向内存中写入数据。

4、一般使用%p以十六进制格式显示内存地址。

如何使用指针:
定义指针变量:

类型* 指针变量名;

int* xxx_p;

char* \ double*

1、指针变量中只记录了内存中某一个字节的地址编号,我们把它当作一个内存块的首地址,当使用指针变量访问内存时具体访问多少个字节,由指针变量的类型决定。

char* p;    // 能访问1字节 
short* p;   // 能访问2字节
int* p;     // 能访问4字节

2、普通变量与指针变量的用法不同,为了避免混用,所以指针变量建议一般以p结尾,加以区分。

3、指针变量不能连续定义,一个*只能定义出一个指针变量。

int num1,num2,num3;
int* p1,p2,p3;      // p1是指针变量,p2、p3是普通的int类型变量
int *p1,p2,p3;
int *p1,*p2,*p3;    // p1、p2、p3都是指针变量
​
typedef int* intp;
intp p1,p2,p3;      // p1、p2、p3都是指针变量

4、指针变量与普通变量一样,默认值是随机的(野指针),为了安全尽量给指针变量初始化,如果不知道该赋什么值,可以先初始化为NULL(空指针)。

int* p1;    //  野指针
int* p2 = NULL; //  空指针

给指针变量赋值:

指针变量 = 内存地址。

所谓的给指针变量赋值,就是给指针变量存储一个内存地址,如果该内存地址是非法的,当使用指针变量访问内存是就会出现段错误。

//指向堆内存
int* p = malloc(4); // 把堆内存的地址赋值给指针变量
​
//指向num所在内存段(data\bss\stack)
int* p = &num;      // &计算出变量的内存地址(单目运算符)
注意:num变量的类型必须与p的类型相同
指针变量解引用:

*指针变量

指针变量赋值就是引用一块内存,解引用就是根据指针变量存储的内存地址,去访问内存,具体访问多少个字节由指针变量的类型决定。

如果指针变量中存储的是非法内存地址,该动作会出现段错误,要从指针变量赋值的步骤去解决。

int num = 1234;
int* p = &num;
​
//*p <=> num;
printf("%d",*p);
*p = 2345;
printf("%d\n",num);
int main()
{
    const int num = 1234;
    int* p = (int*)&num;
    *p = 6666;
    printf("%d\n",num);
}
证明指针变量存储的就是一个整数:
void func(int addr)//long 建议用long类型因为是8字节,指针可能是8字节{
    *(int*)addr = 100;
    printf("%#x\n",addr);
}
​
int main(int argc,const char* argv[]){
    int num = 0;
    printf("%p\n",&num);
    func(&num);
    printf("%d\n",num);
}
​

为什么要使用指针:
1、函数之间需要共享变量

函数之间的命名空间是互相独立,并且是以赋值方式单向传参的(值传递),所以传参无法解决共享变量的问题

全局变量虽然可以在函数之间共享,但过多使用全局变量可能会造成命名冲突和内存浪费。

使用数组还需要额外传递长度

当函数需要返回两个以上的参数(返回值)时,就需要共享变量了(输出型参数)

虽然函数之间命名空间是相互独立的,但是所使用的地址空间是同一个,所以指针可以解决

#include <stdio.h>
//函数通过指针共享变量
int scanf = 100;//定义全局的可能命名冲突
​
void func(int* p)
{
    printf("%d %p\n",*p,p);
    *p = 100;
    printf("%d %p\n",*p,p);
}
​
int main(int argc,const char* argv[])
{
    int num = 0;
    printf("%d %p\n",num,&num);
    func(&num);
    printf("%d\n",num);
}
#include <stdio.h>
#include <time.h>
​
//说明想要获取多个返回值,可以借助指针返回
int _time(int* p)
{
    *p = 1234;
    return 1234;
}
int main(int argc,const char* argv[])
{
    /*
    time_t sec = 0;
    time_t s = time(&sec);
    */
    time_t sec = 0;
    time_t s = _time(&sec);
    printf("%lu %lu\n",s,sec);
}

2、使用指针变量可以提高函数的传参效率

函数之以赋值方式传参的,也就是内存的拷贝,把一个变量的内存内容拷贝给别一个变量,当变量的字节数比较大时(大于4字节),传参效率就很低,而传递变量的地址,只需要拷贝4|8字节内存。

#include <stdio.h>                      
void func(long double f)
{
​
}
int main()
{
    long double f = 3.14;
    for(int i=0; i<1000000000; i++)
    {   
        func(f);
        f++;
    }   
}
/*
real    0m5.527s
user    0m5.523s
sys     0m0.004s
*/
​
void func(long double* f)
{
​
}
int main()
{
    long double f = 3.14;
    for(int i=0; i<1000000000; i++)
    {   
        func(&f);
        f++;
    }   
}
/*
real    0m2.553s
user    0m2.553s
sys     0m0.000s
*/
3、使用堆内存时必须与指针变量配合

堆内存无法取名字,标准库、操作系统提供的内存分配接口函数的返回值都是内存地址,所以必须使用变量配合才能使用堆内存。

void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

注意:由于使用指针变量具有一定的危险,所以除了以上情况,不要轻易使用指针。【学海无涯】

使用指针要注意的问题:
空指针:

指针变量中如果存储的是NULL,那么它就是空指针,因此操作系统规定程序不能访问该内存,只要访问就一定会产生段错误,同时也是返回值是指针类型的函数执行错误的标志。

int* func(void)
{
    if(条件)
    {
         return NULL;   //  返回NULL表示函数执行有误 
    }
}

如何避免空指针产生的段错误?

对来历不明的指针进行解引用前,要先判断是否是空指针。

1、当指针变量接收了函数的返回值,判断是否是空指针,既能避免访问空指针产生的段错误,也能知道该函数执行是否失败、出错。

2、如果设计的函数参数是指针变量,那么调用者传递实参就可能是空指针,对指针变量解引用前要先判断。

if(NULL == p) // 正确写法
{
​
}
​
if(p == NULL) // 错误写法,当少写一个等号时,就变成了给p赋值为空指针
{
​
}
​
if(!p)  //大多数系统的NULL是0,但有少数系统的NULL是1 
{
    
}
【vim /usr/include/stdio.h  查找 :/NULL】
【find /usr/include -name stddef.h】
野指针:

指针变量中存储的地址,无法确定是否是合法的内存地址,这种指针变量被称为野指针。

对野指针解引用的后果:

1、一切正常,指针变量恰好存储的是空闲的内存地址,概率不高。

2、段错误,存储的是非法的内存地址。

3、脏数据,存储的是其它变量的内存地址。

如何避免野指针产生的错误:

野指针无法判断出来,但所有的野指针都是人为制造出来的,所以要想避免野指针产生的错误,只能不制造野指针。

如何不制造野指针:

1、定义指针变量时一定要初始化,要么赋值一个合法的内存地址,要么初始化NULL。

2、不返回局部变量、块变量的地址,当函数执行完毕后,局部变量和块变量就被销毁。

3、与堆内存配合的指针变量,当堆内存被释放、销毁,该指针变量要及时的赋值为NULL。

#include <stdio.h>
//说明函数不能返回栈内存的地址
int* func(void)
{
    int num = 10;
    int* p = &num;
    //return &num;还会有点警告
    return p;
}
​
int main(int argc,const char* argv[])
{
    int* p = func();
    printf("%d\n",*p);
​
    printf("hehehe\n");
    printf("%d\n",*p);//返回的栈内存被上面的函数修改了
}
野指针比空指针的危害更大:

野指针产生的错误具有隐藏性、潜伏性、随机性,所以野指针比空指针危害更大。

作业:

1、实现一个函数,用于交换两个int变量的值。并调用它实现一个数组排序函数

#include <stdio.h>
​
void swap(int* p1,int* p2)
{
    if(NULL == p1 || NULL == p2)
        return;
    
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
​
void sort(int arr[],int len)
{
    for(int i=0; i<len-1; i++)
    {
        for(int j=i+1; j<len; j++)
        {
            if(arr[i] > arr[j])
            {
                swap(&arr[i],&arr[j]);
            }
        }
    }
}
​
int main()
{
    int num1 = 3, num2 = 7;
    printf("%d %d\n",num1,num2);
    swap(&num1,&num2);                                                          
    printf("%d %d\n",num1,num2);
}

2、实现一个函数,用于计算两整数最大公约数和最小公倍数。

#include <stdio.h>
​
// 最大公约数据使用返回值,最小公倍数存储在调用者提供的指针所指向的内存
int max_min(int num1,int num2,int* result)
{
    if(NULL == result)
        return -1;
    /*  
    int max = 1;
    for(int i=2; i<=num1; i++)
    {
        if(0 == num1%i && 0 == num2 % i)
            max = i;
    }
​
    for(int i=num1*num2; i>=num1; i--)
    {
        if(0==i%num1 && 0==i%num2)
        {
            *result = i;
        }
    }
​
    //  *result = num1*num2/max;
    return max;
    */
​
    for(int i=num1*num2; i>=num1; i--)
    {   
        if(0 == i%num1 && 0 == i%num2)
            *result = i;
    }   
​
    return num1 * num2 / *result;
}
​
int main()
{
    int min;                            
    int max = max_min(4,6,&min);
​
    printf("%d %d\n",max,min);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值