有过语言基础的同学都知道C语言是面向过程的,C++是C的超集,有过C、Java或Android基础的同学可以直接跟随本专题学习,如果基础语言不好C语言入门教程可以看链接的教程对C语言学习精通.

基本类型

每种语言都有基本的数据类型,表达方式都是一样的,需要注意在C语言中存在有符号和无符号的区别. 需要注意的是Java 中的long类型在C语言中是long long是8个字节

需要注意在C99标准以前,C语言里面没有bool,C++里面才有,在C99标准里面定义了bool类型需要引入头文件#include <stdbool.h> ,在C/C++中if遵循一个规则,非0为true,非null为true.

    bool sb = true;
    signed int i = 10;//有符号 char int
    unsigned int s = 10;//无符号
    uint32_t ui1 = -1;
    printf("ui1:%u",ui1);//0 - 4294967295 变成最大值
    long ll = 10;
    long long sss = 11;//Java中的long
    long int ss = 11;//=long java中的long = long long,在规定中int 至少要和short一样长,long至少和int一样长
          //C bool 非0即true,非null为true
    printf("%lu",sizeof(ll));// long类型 8个字节 sizeof获取字节数
    printf("Hello, World!\n");
整型 字节 取值范围 占位
int 4 -2,147,483,648 到 2,147,483,647 %d
unsigned int 4 0 到 4,294,967,295 %u
short 2 -32,768 到 32,767 %hd
unsigned short 2 0 到 65,535 %hu
long 4 -2,147,483,648 到 2,147,483,647 %ld
unsigned long 4 0 到 4,294,967,295 %lu
char 1 -128 到 127 %c
unsigned char 1 0 到 255 %c

在上表中,在某些平台的字节大小并不一样,可以使用sizeof(type)得到准确的类型的存储字节的大小

注意:long int 类型其实就是长整型 = long 可以省去int,在标准中,规定int至少和short一样长,long至少和int一样长.

###数组类型

在C/C++ 语言中,定义数组必须给定数组长度,变量的声明是存储在栈中,在Window或Linux系统中栈的大小都是有限制的,在Linux中栈的大小是8192kb,如果超过这个大小就会栈异常 stack overflow

    int a[10];//c中的数组必须给定长度
    int b[] = {1,2,3,4};
    char a[8192*1024];//linux 栈大小是8192kb stack overflow 栈中的大小存在限制 如果超出大小会stack overflow

查询Linux系统栈的大小: 命令 ulimit -a,找到stack size 就是栈的大小

core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
file size               (blocks, -f) unlimited
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 256
pipe size            (512 bytes, -p) 1
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 2784
virtual memory          (kbytes, -v) unlimited

include

在C/C++语言中的include和java中的import相似,但是又存在着一些区别.

如:声明Header2.h头文件

#ifndef Header2_h
#define Header2_h

#include 

void test3();

#endif /* Header2_h */

新建一个.cpp文件Header2.cpp

#include "Header2.h"

void test3(){
    printf("\nTest3");
}

在新建一个Header1.h头文件,include Header2.h

#include "Header2.h"
#ifndef Header1_h
#define Header1_h

void test();

void test1(){
    printf("\ntest1");
}

#endif /* Header1_h */

我们在main.cpp中include Header1.h,就可以调用Header2.h的方法实现

#include 
#include "Header1.h"//Header1.h文件includeHeader2.h,可以直接调用Header2.h的方法
using namespace std;

void test(){
    printf("test");
}

int main(int argc, const char * argv[]) {
    test();
    test1();
    test3();
}

动态内存申请

在上述中,数组类型中 栈的大小是有限制的,我们可以去堆中去申请内存来使用.

malloc

malloc 没有初始化内存的内容,一般需要调用memset来初始化这部分内存空间

 size_t size = 8192*1024;
 //常用的malloc申请内存
 void *jj= malloc(size);//申请大小8192*1024 大小的堆内存空间0 
 memset(jj, 0, size);//将jj指向的内存初始化为0 长度为size

calloc

申请内存并将初始化内存数据为null,calloc 和malloc 逻辑是相同的.

//第一个参数大小:10 * 4 = 40个字节的长度
int *cal = (int*)calloc(10, sizeof(int));
//相当于 void *jj= malloc(sizeof(int)*10); memset(jj, 0, sizeof(int)*10);

realloc

对malloc申请的内存进行大小调整

char *a = (char*)malloc(10);
realloc(a, 20);

free

free 释放malloc和calloc申请的内存空间,并且需要将指针置为0否则可能会悬空指针

free(cal);
cal = 0;

alloca 在栈中申请内存,很少会用到

 int *p_alloca= (int*)alloca(sizeof(int)*10);

alloca无需释放内存空间,因为栈中的变量,在不会使用该变量后,会自动释放内存空间.

上述讲解了常见的动态内存申请,非常简单的API调用,但是我们需要更加深入的去了解内存模型,内存中是如何运行的? 作为C/C++的开发者,必须要掌握内存,因为在非常多的时候都需要去申请内存和释放内存,了解内存模型跟有助于对内存的使用.

Linux下的内存模型

Linux下的内存模型如下,主要有内核空间、栈、动态链接库、堆、全局数据区、常量区、代码区、保留区域.

各个内存区域说明

内存区域 说明
程序代码区 存放函数体的二进制代码,一个C语言程序由多个函数构成,程序的执行就是函数之间的调用.
常量区 存放一般常量、字符串常量.这块内存区域,只有读取权限没有写入权限,它们的值在程序运行期间不能改变.
全局数据区 全局变量、静态变量.这块内存有读写权限,它们的值可以在程序运行任意之间可以改变.
堆区 堆区一般由程序员进行分配和释放,如果程序员不释放,在程序运行结束时,会由操作系统回收.malloc calloc free,操作的就是这块区域内存,也是最重要的部分.
动态链接库 用于在程序运行期间加载和卸载动态链接库.
栈区 存放函数的参数值和局部变量的值等.

程序代码区常量区全局数据区,在程序加载到内存后就分配好了,并在程序运行期间一直存在,不能对其销毁和增加(大小固定),只能等到程序运行结束后由操作系统回收,所以他们在任何地方都能够访问,因为它们在内存中一直存在.

在函数被调用时,会将参数、局部变量、返回地址等与函数信息压入栈中,函数执行结束后,这些信息都将被销毁.局部变量和参数只在当前函数中有效,不能传递到函数外部,因为在函数执行结束后,它们已经在内存中不在了.

常量区、全局数据区、栈上的内存都是由系统自动分配和释放的,不能由程序员控制.程序员唯一能控制的区域就是:堆区.堆区是巨大的一块内存空间,常常占据整个虚拟空间的绝大部分,在这一片的空间内,可以由程序员申请一块内存,自由的存入数据和使用.堆内存在程序主动结束前会一直存在,并不随函数的结束而释放.在函数内部产生的数据只要放在堆中,就可以在函数外部调用.

char *str1 = "jakeprim.cn";//字符串在常量区,str1在全局数据区

int n;//全局数据区

char *func(){
    char *str = "func";//字符串在常量区,str在栈区
    return str;
}

/**
 *加深对内存理解的实例
 */
void eg_ram(){
    int a;//栈区
    char *str2 = "str2";//字符串在常量区 str2在栈区
    char arr[20] = "arr[]";//字符串和arr都在栈区
    char *pstr = func();//栈区
    int b;//栈区
    printf("str1:%#X\npstr:%#X\nstr2:%#X\n",str1,pstr,str2);
    puts("------------------------");
    printf("&str1: %#X\n   &n: %#X\n", &str1, &n);
    puts("--------------");
    printf("  &a: %#X\n arr: %#X\n  &b: %#X\n", &a, arr, &b);
    puts("--------------");
    printf("n: %d\na :%d\nb: %d\n", n, a, b);
    puts("--------------");
    printf("%s\n", pstr);
}

输出:

str1:0XEDE
pstr:0XEEA
str2:0XEEF
------------------------
&str1: 0X2020
   &n: 0X2028
--------------
  &a: 0XEFBFF55C
 arr: 0XEFBFF560
  &b: 0XEFBFF544
--------------
n: 0
a :32766
b: 0
d: 0
--------------
func

函数 func() 中的局部字符串常量"func"也被存储到常量区,不会随着 func() 的运行结束而销毁,所以最后依然能够输出。

字符数组 arr[20] 在栈区分配内存,字符串"arr[]"就保存在这块内存中,而不是在常量区,大家要注意区分。

全局变量的内存在编译时就已经分配好了,它的默认初始值是 0(它所占用的每一个字节都是0值),局部变量的内存在函数调用时分配,它默认初始值是不确定的,由编译器决定,一般是垃圾值.如:n,a,b的值.

内存分配原理

进程分配内存主要由两个系统调用完成:brk和mmap

  1. brk是将_edata(指带堆位置的指针)往高地址推;
  2. mmap是找一块空闲的虚拟内存分配;

brk和mmap的详细讲解

malloc小于128k的内存,使用brk分配内存,将_edata往高地址推,大于128k使用m map.

情况1 malloc分配小于128k的内存

为了方便大家理解,我画了如下图的形式.进程调用A = malloc(20K),malloc函数会调用brk系统调用,将_edata指针往高地址推20K,完成虚拟内存分配.调用B = malloc(30K) 又会将_edata指针往高地址推30K.

情况2,malloc分配大于128K的内存

如下图进程调用D = malloc(200K),这时申请的内存大于128K,malloc函数不会在调用brk去推_edata指针了,而是调用mmap系统调用,在堆和栈之间找一块空闲的虚拟内存进行分配.

那么问题来了,既然brk推指针很方便为什么又要引入mmap呢? 其实主要是因为:brk分配的内存需要等到高地址内存释放后才能释放,例如在B释放之前A是不可能释放的,这就是产生内存碎片的原因,而mmap是单独分配一块内存可以单独释放.当然mmap在性能上要比brk差,因为mmap需要查找空闲的虚拟内存.在具体的可以去glibc查看一下malloc源码

情况3.free释放内存

调用free(D),由于D是单独的一块内存可以直接释放掉虚拟内存和物理内存.

调用free(B),B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,不可能往回推,如果往回推那么C该怎么办呢?当然B这块内存是可以重用的,如果再来一个请求30K的内存空间,malloc很可能把B这块内存直接返回.

当调用free(C),B和C链接起来130K的内存空闲,当最高地址的内存空闲超过128K,会执行内存收紧操作,于是变成了第二个图.

mmap是需要查找一块空闲的虚拟内存进行分配,而brk是在当前的内存位置往上推所需要的内存空间大小,不需要进行查找操作,brk在性能上要比mmap的性能高.

malloc申请内存后,为什么要使用memset?

当有内存释放会存在内存碎片,brk 有可能是之前释放的内存,如上图的B,存在脏数据,memset 会重置内存空间的数据, 一切从0开始.

如果i1 i2 申请40k的字节,值都是100,这时候释放i2,申请i3,同样40k的字节,这时会复用i2的内存碎片,但是如果不使用memset 复用i2的内存碎片di3的值那么都是100.