C语言深度剖析学习笔记-基本数据类型

C语言32个关键字

数据存储类别auto、static、register、extern
基本数据类型void、char、int、float、double
类型修饰符short、long、signed、unsigned
类型限定符const、volatile
组合数据类型struct、union、enum
控制语句if、else、do、while、for、switch、case、default、break、continue、goto、return
其它typedef、sizeof

数据存储类别指示符:

1. 概述

C语言声明变量有存储类型指示符:extern、static、auto、register,不指定时编译器根据约定自动取缺省值auto。
存储类型指示符的位置是任意的,仅对于存储类型指示符而言:

static const int i <=> const static int i

C语言规范中,进行语法分析的时候,typedef和数据存储类别指示符是等价的, 仅对于typedef而言:

typedef const int i <=> const typedef int i <=>
int const typedef i <=> const int typedef i

typedef不能和static等存储类型指示符同时使用,因为每种变量只能有一种存储类型

2. static(静态变量)
  • 修饰局部变量,扩展其生存期到整个程序生存期,但变量作用域不变,作用域外无法访问,位于静态存储区;
  • 修饰全局变量函数,限制其作用域,由整个程序或者项目到仅限于所在cpp文件(对应于内部链接);
  • [C++]修饰类的的数据成员成员函数,表示数据唯一性,即该数据成员或者成员函数属于类而不是某个对象。

linkage(链接性): 描述某个标识符(或名称)在整个程序或者某个源文件中能否绑定到同一实体(某块内存,如具体的变量以及函数体等)
无链接性:
局部变量(包括类的非静态成员), 函数形参.
内部链接(internal linkage): 标识符只能绑定到某个编译单元(预处理后的某个源码文件)内部的实体,不能跨编译单元:
a. 静态全局变量, 全局const常量(C++,C中还是外部链接);
b. inline函数(自由函数和类成员函数), 静态自由函数;
外部链接(external linkage):标识符可以跨编译单元绑定到某一实体:
a. 非静态全局变量, 类的静态数据成员;
b. 非静态自由函数,类成员函数和类静态成员函数;

3. extern(声明其具有外部链接属性,同static相反):
  • 声明本编译单元中的全局变量(一般用于全局变量定义之前使用该全局变量)
  • 声明其它源文件中的全局变量(注意这里的源文件是指cpp文件,不是头文件,头文件直接include就行了,不需要用extern);
  • extern与const同时使用,表示具有外部链接属性的常量.
  • extern "C" {}:用于C++代码,告诉编译器{}中的代码编译时不做名称改写,防止由于C++的名称改写而导致的连接错误;一般用于C语言写的库(要在头文件的声明中使用),并且允许它用于C++语言时.

类型限定符

1. const

只读变量。

  • 实质还是变量,只是不能修改(C中可通过指针间接修改);
  • C中不能用const变量来指定数组元素个数,但C++中可以;
  • 分析const的作用对象时,忽略基本类型说明符即可,所以:
   const int i; <=> int const i;
  • 可以用来说明只读数组;
  • 修饰指针:
const int * p; // => const (*p) <= 指针指向的内存不可修改
int const * p; // => const (*p) <= 指针指向的内存不可修改
int * const p; // => * (const p) <= 指针变量本身的值不可修改
const int * const p; // => const (* (const p)) <=  指针变量本身和指针指向的内存都不可修改
  • 修饰函数参数: 防止实参被函数修改;
  • 修饰函数返回值: 返回值不可被改变,一般用于返回引用的函数,防止外部修改被引用的变量;
  • const与#define
    编译器不会为const只读变量分配存储空间,而是放在符号表中,效率高,const定义的只读变量从汇编角度来看,只是给出了对应的内存地址, 而不是像define一样给出的是立即数,const只读变量在程序运行期间只有一份拷贝,因为它是全局只读变量,存放在静态区:
#define M 3 // 宏常量
const int N = 5; // 此时未将N放入内存中
// ... main ...
int I = M; // 预编译期间进行宏替换,分配空间
int J = M; // 再进行宏替换,又一次分配内存
int i = N; // 此时为N分配内存,以后不再分配!
int j = N; // 没有内存分配
2. volatile

关闭优化,让编译器老老实实按语句的意思来编译。

[global] int i = 0;   |         |
[local]  i = 20;      |         |  
                      | <====== |     i = 5; // 端口状态发生改变
         int j = i;   |         |
         int k = i;   |         |

i = 20; 从内存中取出i的值赋值给i;
int j = i; 使用前面取出的值而不是重新从内存读取值,如果此时端口值(i)发生变化,那此处使用的i就不是i的真实值; 如果把int i = 0 改为 volatile int i = 0,那编译器就不会对i做优化,此时会从内存中重新读取i的值,然后给j赋值。

组合数据类型

1. 结构体
  • 空结构体大小为1;
  • 柔性数组:当结构体的最后一个元素(并且不是第一个,前面有其它成员)是数组时,允许它不指定大小,即它的大小是可变的,成为柔性数组:
    • sizeof带柔性数组的结构体,大小不包括柔性数组大小;
    • 带柔性数组的结构体需要用malloc进行内存分配,并且分配的大小要大于结构体大小,多出来的部分就是柔性数组的大小;
2. 结构体的内存对齐

默认情况下,为了方便对对结构体内元素的访问与管理,当结构体内的元素长度都小于处理器的位数时,便以结构体里最长的元素为其单位,即结构体的长度一定是最长数据元素的整数倍;

如果结构体里最长元素的内存长度大于处理器位数,就以处理器的位数为对齐单位。

并不是处理器位数,是看操作系统的位数

// manjaro x86_64 -> 64 bits = 8 bytes
struct foo1 {                       | base_addr: 0
    char *p; // 8 bytes             | offset: 0
    char c;  // 1 bytes             | offset: 8
//  char pad[7] // padding 7 bytes  |
    long x;  // 8 bytes             | offset: 16
};
// total 24 bytes

struct foo3 {
    int i; // 4 bytes (min(4,8) = 4)
    char c; // 1 bytes
/*  char pad[3]  */ // 结构体的尾填充 3 bytes
};
// total: 8 bytes

foo5和foo6中的最长元素长度是int的长度(换句话说看的是类型长度,不是变量长度),所以foo5和foo6的长度一定是4的整数倍。

对于位域,比如foo6中a和b的类型是int,占4个字节,而a、b只占去了1个字节,占去的是这四个字节中的低位字节,也就是说,位域是通过偏移来实现的,而这个偏移是从低位向高位偏移,即a占据低位的0-4bits,b占据低位的5-7bits;

对于位域,我觉得只要把它以bytes为基本单位来分析,同时时刻铭记它是在一个内存模型(数据类型)中即可.

struct foo5 {
    short s;      // 2 bytes
    char c;       // 1 bytes
    int flip:1;   // 1 bits
    int nybble:4; // 4 bits
    int pad1:3;   // padding 3 bits // 用来与char c对齐,看成一个独立的bytes
    int septet:7; // 7 bits
//  int pad2:17;  // padding 17 bits // 4bytes是一个集体,最后没用完的要填充,形成一个完整的4bytes
//  char pad3;    // padding 8 bits
};
// total: 8 bytes
---------------------------------------------------------------
|    short    |  char  |1| 4 |3|   7   |     17      |  char  |
-----------------------|-------|---------------------|--------|
                        1 bytes         3 bytes        1 bytes
|------------------------------|------------------------------|
            4 bytes                        4 bytes

struct foo5_2 {
    short s;      // 2 bytes
    char c;       // 1 bytes
    int flip:2;   // 2 bits ------------|
//  int pad1:6;   // padding 6 bits ----| total 1 byte ---|
    int nybble:9; // 9 bits ---|                          |
//  int pad2:7;   // ----------| total 2 bytes -----------|        
//  int pad3:8;   // ----------| total 1 byte ------------| total 4 bytes (int)
//  int pad4:8;   // padding total 23 bits
    int bb;
    int septet:7; // 7 bits
//  int pad5:1;   // 1 its
    char dd;
//  char pad6[2]; // 2 bytes
}; // length =  8 bytes

struct foo6 {
    int a:5; // 5 bits
    int b:3; // 8 bits
//  int pad:24; // padding 24 bits
};
// total: 4 bytes

struct foo7 {
    int a:30; // total 30 bits
//  int pad1:2; // 填充2个
    int b:3;  // total 33 bits > 32 bits装不下,所以从新的字(int)开始
//  int pad2:29;
};

由于内存对齐的存在,我们在设计结构体或者C++的类时(C++中的结构体和类也有内存对齐), 就需要细心了. 比如下面的foo8占用24bytes, 但是通过调整成员顺序得到foo9, 它的内存占用就变为了16bytes, 这是多么客观!

class foo8 {
    char c;
//  char pad1[7];     // padding 7 bytes aligning to p;     
    struct foo8 *p;
    short x;
//  char pad2[6];    // padding 6 bytes aligning to boundary    
};
// sizeof(foo8) = 24

struct foo9 {
    struct foo9 *p;
    short x;
    char c;
//  char pad[5];   // padding 5 bytes aligning to boundary 
};
// sizeof(foo8) = 16

详细的结构体内存对齐知识,可以研读失传的C结构体打包技艺,这是我从ludx那fork过来的,不是我翻译的,里面有原文链接,另外clang编译器有个“-Wpadded”参数,加上它就会在编译输出信息中以警告的形式输出有关内存对齐的信息。

3. 结构体中的匿名成员

结构体中的匿名联合体或者结构体的成员,可以当做该结构体成员来访问,但是非匿名的就不能直接当成成员访问了.

enum VarEnum {
    T_EMPTY = 0,
    T_DOUBLE = 1
}; // 4 bytes

struct VT_VARIENT {
    enum VarEnum varType;
//  char pad[4]; // 填充空间,浪费掉
    union { // 结构体中的匿名联合体,可以当做结构体的成员来访问,
            // 但是如果不是匿名的,则不能这样!!
        double dbVal;
        int nVal;
        int boolVal:1;
    };
}; // 16 bytes

struct ParamValue {
    char name; // 10 bytes
//  char pad[7]; // 填充空间,浪费掉
    struct VT_VARIENT value;
    struct {
        int i;
        char ch;
    };
}; // 24 bytes

int main(void)
{
    // 结构体可以这样初始化,如果是按顺序来,可以不加.var这样的成员名字
    struct ParamValue dstY = {.name = 'l', .value = {.varType = T_EMPTY, .nVal = 0}, .i = 100, .ch = 'A'};
    printf("%d, %c\n", dstY.i, dstY.ch);
    printf("%d\n", dstY.value.nVal);
    dstY.value.nVal = 10;
    printf("%d\n", dstY.value.nVal);

    printf("sizeof(enum VarEnum) = %lu\n", sizeof(enum VarEnum));
    printf("sizeof(struct VT_VARIENT) = %lu\n", sizeof(struct VT_VARIENT));
    printf("sizeof(struct ParamValue) = %lu\n", sizeof(struct ParamValue));
    printf("sizeof(double) = %lu\n", sizeof(double));

    return 0;
}
4. union
  • 所有成员共享一块内存空间,同一时间只能存储其中的一个数据成员,所有的数据成员具有相同的起始地址(等于union的起始地址);
  • union所占字节数等于其最大成员所占字节数;
  • 用union检测大小端(big_endian,little_endian):
int IsLittleEndian()
{
    union un_ {
        short sh;
        char ch;
    } check;

    check.sh = 0x01;
    if (check.ch == 0x01) {
        return 1;
    } else {
        return 0;
    }
}

大端模式和小端模式:

几乎所有的机器上,多字节对象都被存储为连续的字节序列,例如C语言中,int型变量x的地址为0x100,则&x=0x100,这个0x100是指int所占的四个字节中地址最低的那个字节的内存地址(基址),与内存增长顺序无关。也就是说,x的四个字节将被存储在存储器的0x100,0x101,0x102,0x103的位置

而存储地址内的排列则有两种通用规则:大端模式和小端模式
大端:低地址存高字节
小端:低地址存低字节

3. enum
  • 枚举的成员称为枚举常量,枚举常量的值只能取整型值,不能是浮点值;
  • 枚举里面的成员是可以取到的值,跟int变量可以取 … -2 -1 0 1 2 … 一样,所以sizeof一个enum的结果是4,因为默认成员是int类型;
  • 注意:
    • struct,union成员都是用分号“;”分割,而enum是用逗号“,”分割的;
    • struct,class,union,enum的{}后面要加“;”。
  • 用enum标示插件或者选项:
enum flag_ {
    ATE   = 0x00000001;
    APP   = 0x00000002;
    IN    = 0x00000004;
    OUT   = 0x00000008;
    BIN   = 0x00000016;
    TRUNC = 0x00000032;
} flag;
...
flag option = OUT | ATE | BIN;
bool open(const char* filename, const flag option)
{
    if (option & ATE == ATE) { // 设置了此选项
        ...
    }
    if (option & APP == APP) { // 设置了此选项
        ...
    }
    ...
}

类似于上面代码的复合选项功能,插件系统中也可以用enum标示哪些插件启用了,哪些没启用,启用一个就按位或(|)上对应标识,关闭一个就按位与(&)上对应标识的按位取反(~)

控制语句

  • default语句不应省略,而且要用来处理真正的默认情况(或者未定义的行为);

  • case 后面只能是整型或字符型的常量或常量表达式,const变量不可以;

  • 多重循环中,尽可能将长循环放在外层,最短的放最内层;

  • 函数如果无参数,应当显示用void指明;

  • 所有函数都应该显示给出返回值,不应以来编译器特性(构造,析构除外)。

typedef

给已经存在的类型起别名

1. typedef与类型限定符const
typedef struct student
{
    // Code
} student_st, *student_pst;
...
student_st stu1; // <=> struct student stu;
student_pst stu2; // <=> struct student *stu <=> student_st *stu;
const student_pst stu3; // => const stu3, 对于编译器来说,student_pst就是个类型说明符, 我们分析的时候直接去掉就行,跟int等一样.
student_pst const stu4; // => const stu4
2. typedef与#define
#define INT32 int
typedef long INT64;
unsigned INT32 i = 10; // no problem
unsigned INT64 j = 10; // 不行, typedef的类型不支持用类型修饰符进行扩展
#define PCHAR char*
typedef char* PCHAR2;
PCHAR p1, p2; // => char* p1, p2 => p2不是指针,是char变量
PCHAR2 p3, p4; // p3, p4都是指针,typedef定义的就是数据类型,与int等的地位用法是一样的。

sizeof

sizeof()求值是在编译期, 所以类似如下代码是没问题的:

int a[10] = {0};
size_t size_a = sizeof(a[15]); // <=> sizeof(int)

但是如果通过函数而不是sizeof()来使用a[15]就会出问题,因为函数是运行期执行的,运行期a[15]明越界了.

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值