c 语言基础知识

2018-02-11

主要对旧知识对温习和知识盲点的记录。(部分知识来自网络)

1. 基础知识

1.1. 变量字节数

有符号 无符号 32位字节数 64位字节数
char unsigned char 1 1
short unsigned short 2 2
int unsigned int 4 4
long unsigned long 4 8
int32_t uint32_t 4 4
int64_t uint64_t 8 8
char* \ 4 8
float \ 4 4
double \ 8 8

1.2. 位运算

符号 描述 应用
& 与运算(两个 1 是 1,否则为 0),可以截取一个尾数的后面几位数字 idx = h & d->ht[table].sizemask;
flags &= ~O_NONBLOCK;
| 或运算(有一个为 1,为 1) flags | = O_NONBLOCK;
^ 异域运算(相同的两个位是 0,不同 1)  
~ 取反运算 flags &= ~O_NONBLOCK;
« 左移运算(左移一位相当于 * 2) #define ZIP_INT_32B (0xc0 | 1«4)
» 右移运算(右移一位相当于 除以 2) ret = i32»8;

1
2
3
4
5
6
7
8
9
10
11
12
// 可以灵活运用位运算,将多项属性存储在一个变量里。
// 二进制,每一位 1 表示一个选项。 | 操作表示添加一项。 & ~ 结合使用去掉一项。
bool set_module_key(const string& value) {
    if (value.size() > 90) {
        m_ull_has_bit &= ~0x00000020;
        m_ui_error = 0;
        return false;
    }
    m_str_module_key = value;
    m_ull_has_bit |= 0x00000020;
    return true;
}

1.3. static

static 全局变量和全局变量,static 局部变量和局部变量,static 函数和普通函数区别:

  1. 全局静态变量 :
    1. 在全局数据区内分配内存。
    2. 如果没有初始化,其默认值为0。
    3. 该变量在本文件内从定义开始到文件结束可见。
  2. 局部静态变量:
    1. 该变量在全局数据区分配内存。
    2. 如果不显示初始化,那么将被隐式初始化为0。
    3. 它始终驻留在全局数据区,直到程序运行结束。
    4. 其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
  3. 静态函数:
    1. 静态函数只能在本源文件中使用。
    2. 在文件作用域中声明的inline函数默认为static。

      说明:静态函数只是一个普通的全局函数,只不过受static限制,他只能在文件所在的编译单位内使用,不能在其他编译单位内使用。


1.4. union 共同体

共同体理解,一个数据结构有多个成员数据,但是只能保存一个成员数据。共同体结构的大小,是最大的成员的大小。

1
2
3
4
5
union Data {
    int i;
    float f;
    char str[20];
} data;

1.5. 字节对齐

参考引用文章,理解对齐的规律。 理解 字节对齐你真明白了么 谈谈内存对齐 C语言字节对齐问题详解 谈谈内存对齐一


nginx 对齐操作

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef unsigned char u_char;

#define NGX_POOL_ALIGNMENT 16
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

int main(int argc, char *argv[]) {
    ...
    char *p = (char *)malloc(1024);
    u_char *p10 = ngx_align_ptr(p, 4);
    u_char *p11 = ngx_align_ptr(p+1, 4);
    ...
}

1.6. 宏

  • 宏的作用和展开
  1. C 语言一般用 #define 宏作为常变量;C++里用 const 修饰常变量。
  2. 宏定义只是做字符替换,不分配内存空间。
  3. 预编译,编译,汇编器,链接;预编译的时候编译器将宏对应的值替换到代码中去。
  4. 宏一般只是替换对应的值,对定义不做正确性检查,有一定危险性。
  • C 语言编程中以空间换时间() 计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有可以利用C语言编程中以空间换时间,使用的时候可以直接用指针来操作。同时我们也可以使用宏函数而不是函数。

  • ‘#’ 和 ‘##’ 作用 使用 # 把宏参数变为一个字符串,用 ## 把两个宏参数贴合在一起

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

#define STR(s) #s
#define CONV(a, b) (a##e##b)

int main() {
    std::cout << STR(12345) << std::endl;
    std::cout << CONV(2, 3) << std::endl;
    return 0;
}

结果:

1
2
12345
2000
  • 宏函数 函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。

max(++a, b) 这样的操作会隐藏了错误。例如下面这个例子: 举例:

1
2
3
#define max(x, y) (x) > (y) ? (x) : (y)
void func(int a) { printf("%d\n", a); }
func(max(a, ++b));

==>

1
func((a) > (++b) ? (a) : (++b));

1.7. volatile

volatile关键字是一种限定符用来声明一个对象在程序中可以被语句外的东西修改,比如操作系统、硬件或并发执行线程。遇到该关键字,编译器不再对该变量的代码进行优化,不再从寄存器中读取变量的值,而是直接从它所在的内存中读取值,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。好,说完了。一句话总结一下,volatile到底有什么用。它的作用就是叫编译器不要偷懒,去内存中去取值。

  1. 易变性。 在汇编层面观察,两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是直接从内存中读取。
  2. 不可优化性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。相对于前面提到的第一个特性:”易变”性,”不可优化”特性可能知晓的人会相对少一些。
  3. 顺序性。C/C++ Volatile关键词前面提到的两个特性,让Volatile经常被解读为一个为多线程而生的关键词:一个全局变量,会被多线程同时访问/修改,那么线程内部,就不能假设此变量的不变性,并且基于此假设,来做一些程序设计。当然,这样的假设,本身并没有什么问题,多线程编程,并发访问/修改的全局变量,通常都会建议加上Volatile关键词修饰,来防止C/C++编译器进行不必要的优化。变量赋值的时序性在多线程中是很重要的,一般全局变量设置成 Volatile 防止编译器的优化,解决方案,全局共享的数据,在多线程环境下的逻辑需要加锁。

volatile 关键字 volatile关键字与竞态条件和sigchild信号 Volatile关键词深度剖析

可以用 gdb 查看发汇编或者

1
layout split

或者输出汇编代码查看

1
gcc -S test.cpp -o test.s
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

volatile int bbbb = 789;
volatile int aaaa = 123;

int main(int argc, char** argv) {
    aaaa += 456;
    bbbb = aaaa;
    return 0;
}

1
2
3
gdb server -tui
layout regs
set disassemble-next-line on

GDB 单步调试汇编 lldb 命令


2. 其它

2.1. strcpy 实现

参考内核实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

char *_strcpy(char *dest, const char *src) {
    char *tmp = dest;

    while ((*dest++ = *src++) != '\0') {
    }
    return tmp;
}

int main() {
    char dest[100];
    const char *src = "1234567890";
    printf("return: %s\n", _strcpy(dest, src));
    return 0;
}

2.2. glibc 如何管理内存

slab 内存管理机制理解 《深入理解计算机系统》第九章 虚拟内存,9.9 动态内存分配 nignx 内存池

glibc 是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准。

glibc 通过预分配大块内存来解决内存申请效率,大块内存切成小块方式进行分配,小块内存回收后,空闲链表维护空闲内存块供后续的内存申请,空闲内存块或被合并空闲块。 问题:glibc 内存管理容易造成内存碎片,导致内存泄漏。