文先生的博客 求职,坐标深圳。(wenfh2020@126.com)

c 语言基础知识

2018-02-11

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

1. 基础知识

1.1. 变量字节数

64 位系统。

有符号 无符号 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 命令


1.8. 字符串长度

主要测试指针和数组保存的字符串长度。

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

int main() {
    char array[] = "1234567890";
    const char* p = "1234567890";
    std::cout << "size: " << sizeof(p)
              << ",  str len: " << strlen(p) << std::endl;
    std::cout << "size: " << sizeof(array)
              << ", str len: " << strlen(array) << std::endl;
    return 0;
}
  • 结果:
1
2
size: 8,  str len: 10
size: 11, str len: 10
  • 分析。
  1. 64位机器,指针长度 8 个字节。
  2. 字符串长度是通过 ‘\0’ 结束符计算的。
  3. 数组除了字符串,还有结束符 ‘\0’。

2. 常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 判读字符是否字母。 */
int isalpha(int c);

/* 判读字母数字。 */
int isalnum(int c);

/* 大小写转换。 */
int tolower(int c);
int toupper(int c);

/* 判读字符是否为数字。 */
int isdigit(int c);

/* 字符串转换整数。 */
int atoi(const char* str);

/* 字符串转换指定格式的数字为十进制。 */
long int strtol (const char* str, char** endptr, int base);

3. 字符串

3.1. strcpy 实现

参考内核实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* gcc test.c -o test && ./test */
#include <iostream>
#include <memory>

char* str_cpy(const char* src, char* dst) {
    if (src == nullptr || dst == nullptr) {
        return nullptr;
    }
    char* tmp = dst;
    while ((*dst++ = *src++) != '\0') {}
    return tmp;
}

int main() {
    const char* src = "hello world!";
    auto dst = std::unique_ptr<char[]>(new char[32]);
    std::cout << str_cpy(src, dst.get()) << std::endl;
}

3.2. 字符串去掉空格符

字符串去掉空格符,返回空格个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

int trim_space(char* s) {
    if (s == nullptr) {
        return 0;
    }

    char* p = s;
    int space_cnt = 0;

    while (*s != '\0') {
        if (*s == ' ') {
            s++;
            space_cnt++;
        } else {
            if (p != s) {
                *p++ = *s++;
            }
        }
    }
    *p = '\0';
    return space_cnt;
}

int main() {
    char data[] = " 22 3  12";
    int num = trim_space(data);
    std::cout << "num: " << num << std::endl;
    std::cout << data << std::endl;
    return 0;
}

3.3. 获取字符串最后一个单词长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

int get_last_word_len(const char* src) {
    if (src == nullptr) {
        return 0;
    }
    const char *p1 = nullptr, *p2 = nullptr;

    while (*src != '\0') {
        // 从左到右找 不为空 的字符
        while (*src == ' ' && *src != '\0') {
            src++;
        }

        // 找到了单词 左端,再找单词 右端
        if (*src != '\0') {
            p1 = src;

            // 从左到右找 空字符
            while (*src != ' ' && *src != '\0') {
                src++;
            }

            p2 = src - 1;
        }
    }

    return p1 ? p2 - p1 + 1 : 0;
}

int main() {
    const char* data = " fds fhsjd    ";
    int len = get_last_word_len(data);
    std::cout << "len: " << len << std::endl;
    std::cout << data << std::endl;
    return 0;
}

4. 算法

4.1. 单链表逆序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct node_s {
    int data;
    struct node_s* next;
} node_t;

node_t* reverse(node_t* head) {
    if (head == nullptr) {
        return nullptr;
    }
    node_t *prev = nullptr, *next = nullptr;
    while (head != nullptr) {
        next = head->next;
        head->next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

4.2. 链表节点交互位置

1 -> 2 -> 3 -> 4 -> 5 -> 6 ==> 2 -> 1 -> 4 -> 3 -> 6 -> 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <iostream>
#include <memory>

// 单链表,k 个节点组内的逆序
template <typename T>
class List {
   private:
    class Node;
    using NodePtr = std::shared_ptr<Node>;

    class Node {
       public:
        Node(const T& data, NodePtr next)
            : m_data(data), m_next(next) {
        }
        T m_data;
        NodePtr m_next = nullptr;
    };

    int m_size = 0;
    NodePtr m_head = nullptr;
    NodePtr m_tail = nullptr;

   public:
    List() {}

    void pushTail(const T& data) {
        auto node = std::make_shared<Node>(data, nullptr);
        if (m_tail) {
            m_tail->m_next = node;
            m_tail = node;
        } else {
            m_head = m_tail = node;
        }
        m_size++;
    }

    // 交换节点的个数。k = 2
    void reverseKGroup(int k) {
        if (m_head == nullptr || k <= 0) {
            return;
        }

        // 第一组是否满足条件。
        auto start = m_head;
        auto end = getkGroupEnd(start, k);
        if (end == nullptr) {
            return;
        }

        m_head = end;
        reverse(start, end);

        auto last_end = start;
        while (last_end->m_next != nullptr) {
            start = last_end->m_next;
            end = getkGroupEnd(start, k);
            if (end == nullptr) {
                return;
            }
            reverse(start, end);
            last_end->m_next = end;
            last_end = start;
        }
        m_tail = last_end;
    }

   private:
    // 反转链表区间
    void reverse(NodePtr start, NodePtr end) {
        if (start && end) {
            end = end->m_next;
            NodePtr cur = start, pre = nullptr, next = nullptr;
            while (cur != end) {
                next = cur->m_next;
                cur->m_next = pre;
                pre = cur;
                cur = next;
            }
            start->m_next = end;
        }
    }

    // 获取 k 个点的组
    NodePtr getkGroupEnd(NodePtr start, int k) {
        while (--k > 0 && start != nullptr) {
            start = start->m_next;
        }
        return start;
    }
};
  • 本文作者: wenfh2020
  • 本文链接: https://wenfh2020.com/2018/02/11/c/
  • 版权声明: 本文可以(非商业)自由拷贝,转载,请注明出处,谢谢! 🤝

作者公众号
微信公众号,干货持续更新~