编译链接一个最小的C程序

在Linux下开发一般都会用到各种各样的库. 比如: 标准库(glibc), 其他第三方库等. 有的小伙伴可能好奇, 怎样开发一个不使用第三方库的程序呢? main函数是不是第一个执行的函数呢? 下面看一个例子, 以上的问题都能够有一个个明确的答案.

  • bar.c 文件
extern void foo(void);
int _start(void) {
    foo();
    return 0;
}

上面的函数_start是程序的入口, 为什么不是main函数呢? 其实程序的入口是可以通过gcc参数自定义的, 此处为_start函数. 而且自定义的程序入口点在linux下也不是程序执行的第一条代码. 之前还有程序二进制文件的加载代码, 动态链接库相关代码, bss段初始化代码等. 但如果是裸机代码, 自定义的程序入口点是程序执行的第一条代码.

  • foo.s 文件
.global foo
foo:
    movl $1, %eax       # write (
    movl $1, %edi       #   fd=1,
    movq $s, %rsi       #   buf=s,
    movl $(e-s), %edx   # count=e-s,
    syscall             # );

    movl $60, %eax      # exit (
    movl $1, %edi       #   status=1
    syscall             # );

s:
    .ascii "\033[01;31mHello, world\033[0m\n";
e:

上面代码的功能是: 通过系统调用将字符串输出到标准输出.

  • Makefile 文件
all: foobar

foobar: foo.o bar.o
    ld -e _start -o $@ $^

%.o: %.s
    as $< -o $@

clean:
    rm -f *.o foobar

上面的ld -e _start则定义了程序的入口点. 通过上面编译得到的可行文件是:

  • 静态程序
  • 没有连接任何库
  • 代码入口点不是main而是_start

如果觉得有帮助, 可以扫描右边的微信打赏码支持一下.

C++ exception

C++ 通过异常来传播错误. 通过异常的方式, 错误会一层一层的向上抛出. 带来的问题是: 如果捕获不及时, 函数调用栈会不断弹出, 直到捕获或程序退出为止. 这种让错误不断蔓延的方式, 很容易造成资源泄漏的问题. 如: 在代码中检查返回值进行错误处理的方法, 将面临抛异常带来的风险. 但通过RAII处理资源释放, 能解决部分问题. 通过返回值检查错误抛异常两种方式混合进行错误处理将增加不必要的复杂度. 既然C++选择了异常错误处理的方式, 写代码时也遵循C++的逻辑为好.

下面是实现异常的实例:

#include <iostream>

class an_execption: public std::exception {
    public:
        const char* what() const noexcept override {
            return "I an exception";
        }
};

int main(void) {
    try {
        throw an_execption();
    } catch(std::exception &e) {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

如果觉得有帮助, 可以扫描右边的微信打赏码支持一下.

C++ new错误处理

在C++中处理内存分配错误大概有两种方法:

  • 使用try...catch
#include <iostream>

int main(void) {
    try {
        long long int n = 1024 * 1024 * 1024, m = 1024 * 1024;
        auto a = new int[n * m];
    } catch(std::bad_alloc &e) {
        std::cout << "catch bad_alloc exception" << std::endl;
    } catch(std::exception &e) {
        std::cout << e.what() << std::endl;
    } catch(...) {
        std::cout << "unknow error" << std::endl;
    }

    return 0;
}
  • 使用nothrow, 错误处理流程与C语言一样
#include <iostream>

int main(void) {
    long long int n = 1024 * 1024 * 1024, m = 1024 * 1024;
    auto a = new(std::nothrow) int[n * m];
    if (a == nullptr) {
        std::cout << "nullptr" << std::endl;
    }

    return 0;
}

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ SFINAE

SFINAE的全称是: Substitution Failure Is Not An Error. 在进行模板匹配时, 可能会出现错误. 而当出现错误时, 不抛出错误, 而是继续去匹配别的模板. 这种情况就是SFINAE. 下面看一个例子:

#include <iostream>

// 仅仅是为了获取T的类型
template <bool B, typename T = void> struct enable_if {};
template <typename T> struct enable_if<true, T> { using type = T; };

// 判断是否有函数A()
template <typename T> struct has_func_A {
    // decltype返回最后一个表达式的结果, 即: bool()对象的类型
    // test的int参数是为了使用可变参数函数,因为模板函数的优先级大于可变参数函数
    template <typename C>
    static constexpr decltype(std::declval<C>().A(), bool()) test(int) {
        return true;
    }

    // black hole
    template <typename C> static constexpr bool test(...) { return false; }

    static constexpr bool value = test<T>(int());
};

// black hole
void O(...) { std::cout << "O" << std::endl; }

// 根据是否存在函数A, 来生成不同的重载函数
template <class T>
typename enable_if<has_func_A<T>::value, int>::type A(const T &obj) {
    return obj.A();
}

template <class T>
typename enable_if<!has_func_A<T>::value, void>::type A(const T &obj) {
    return O(obj);
}

class B {};

class C {
    public:
    int A() const {
        std::cout << "C" << std::endl;
        return 0;
    }
};

int main(void) {
    B b;
    C c;
    A<B>(b); // 输出: O
    A<C>(c); // 输出: C
}

上面的例子, 通过编译器类型推倒机制, 根据不同的条件生成不同的模板函数, 实现灵活的函数重载. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::forward

std::forward的作用是实现参数转发功能. 我们知道参数可能是引用或右值引用, 在函数内部再次传递该参数数时, 我们希望参数还是保持原来的状态. 那就需要用使用std::forward进行参数转发. 下面看一个例子更加直观:

#include <iostream>

struct A {
    A(int &&n) { std::cout << "rvalue = " << n << std::endl; }
    A(int &n) { std::cout << "lvalue = " << n << std::endl; }
};

template <typename T, typename U> void forward(U &&u) {
    T(std::forward<U>(u));
}

template <typename T, typename P, typename... U> void forward(P && p, U &&... u) {
    T(std::forward<P>(p));
    forward<T>(std::forward<U>(u)...);
}

int main() {
    int i = 1;
    forward<A>(i, 2, 3);
}

输出:

lvalue = 1
rvalue = 2
rvalue = 3

从上面的输出可以看出, 变量 i 传入 forward 函数前是左值, 再次传入 A 的构造函数时是左值引用. 而右值 2和3 直到传入 A 的构造函数时都是右值引用. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

参考:

C++ std::weak_ptr

在C++中std::shared_ptr可以实现多个对象共享同一块内存, 但存在循环引用的问题. 即两个shared_ptr互相指向对方, 导致引用计数无法被递减到0, 造成内存泄露. 下面看一个有问题的例子:

#include <iostream>
#include <memory>

class Any {
  public:
    friend bool set_relationship(std::shared_ptr<Any> & p1,
                                 std::shared_ptr<Any> & p2) {
        if (!p1 || !p2) {
            return false;
        }

        p1->m_another = p2;
        p2->m_another = p1;
        return true;
    }

  private:
    std::shared_ptr<Any> m_another;
};

int main(void) {
    auto p1 = std::make_shared<Any>();
    auto p2 = std::make_shared<Any>();
    set_relationship(p1, p2);
    return 0;
}

通过valgrind 工具进行内存检查:

==1996== LEAK SUMMARY:
==1996==    definitely lost: 32 bytes in 1 blocks
==1996==    indirectly lost: 32 bytes in 1 blocks
==1996==      possibly lost: 0 bytes in 0 blocks
==1996==    still reachable: 0 bytes in 0 blocks
==1996==         suppressed: 0 bytes in 0 blocks

使用shared_ptr导致循环引用, 的确造成了内存泄露.

下面看一个使用weak_ptr的例子:

#include <iostream>
#include <memory>

class Any {
  public:
    Any(int a) : m_a(a) {}

    friend bool set_relationship(std::shared_ptr<Any> &p1,
                                 std::shared_ptr<Any> &p2) {
        if (!p1 || !p2) {
            return false;
        }

        // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
        p1->m_another = p2;
        p2->m_another = p1;
        return true;
    }

    void show(void) { std::cout << m_a << std::endl; }

    std::weak_ptr<Any> &get_ptr(void) { return m_another; }

  private:
    int m_a;
    std::weak_ptr<Any> m_another;
};

int main(void) {
    auto p1 = std::make_shared<Any>(1);
    auto p2 = std::make_shared<Any>(2);
    set_relationship(p1, p2);

    assert(p1.use_count() == 1);
    assert(p2.use_count() == 1);

    p1->get_ptr().lock()->show(); // 输出: 2
    return 0;
}

通过valgrind 工具进行内存检查:

==4775== HEAP SUMMARY:
==4775==     in use at exit: 0 bytes in 0 blocks
==4775==   total heap usage: 3 allocs, 3 frees, 72,768 bytes allocated
==4775==
==4775== All heap blocks were freed -- no leaks are possible

通过 weak_ptr 能够解决 shared_ptr 循环引用带来的内存露问题. 如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::shared_ptr

在C++中shared_ptrstd::unique_ptr类似. std::shared_ptrstd::unique_ptr的主要区别在于前者是使用了引用计数, 可以跟踪引用数目. 这意味着, 可以有多个std::shared_ptr实例指向同一块动态分配的内存, 当最后一个引用对象离开其作用域时, 才会释放这块内存. 值得注意的是, std::shared_ptr不能用于管理C语言风格的动态数组. 下面看一使用的例子:

#include <cassert>
#include <iostream>
#include <memory>

class Any {};

int main(void) {
    auto p1 = std::make_shared<Any>();
    assert(p1.use_count() == 1);
    {
        auto p2 = p1;
        std::shared_ptr<Any> p3;
        p3 = p1;
        assert(p1.use_count() == 3);
        assert(p2.use_count() == 3);
        assert(p3.use_count() == 3);
    }
    assert(p1.use_count() == 1);
    return 0;
}
  • shared_ptr 智能指针只能通过赋值构造函数才能增加引用计数. 如果同一指针分别赋值给不同的 shared_ptr 会造成double free的错误.
  • 对同一个对象共享所有权的 shared_ptr 在多个线程上的析构不需要外部加锁保护, 因为引用计数本身修改是原子操作.

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ std::unique_ptr

在C++中使用智能指针能有效避免空指针和悬垂指针出现, 使用智能指针也能减少心智负担. 下面将讲解一下std::unique_ptr智能指针. 该智能指针用于处理只有一个实例拥有所有权的情况, 可以通过所有权转移的方式移交资源. 下面看一下它的使用方法:

#include <iostream>
#include <memory>
#include <cassert>

class Any {
  public:
    friend std::ostream &operator<<(std::ostream &out, const Any &a);
};

std::ostream &operator<<(std::ostream &out, const Any &a) {
    out << "I am Any";
    return out;
}

int main(void) {
    std::unique_ptr<Any> a{new Any};
    std::cout << *a << std::endl;

    std::unique_ptr<Any> a2; // 初始化为nullptr

    // a2 = a; // 非法, 不允许左值赋值
    a2 = std::move(a);
    assert(a == nullptr);
    assert(a2 != nullptr);

    // C++14 可以使用 make_unique函数
    auto a3 = std::make_unique<Any>();
    std::cout << *a3 << std::endl;

    // 创建一个包含实例的数组
    auto a4 = std::make_unique<Any[]>(2);
    std::cout << a4[0] << std::endl;
    std::cout << a4[1] << std::endl;
    return 0;
}

默认使用newdelete来分配和释放内存, 也可以修改该行为. 下面看一个例子:

template <typename T> auto dealloc = [](T *p) { free(p); };

int main(void) {
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    std::unique_ptr<int, decltype(dealloc<int>)> ptr{p, dealloc<int>};
    std::cout << *ptr << std::endl; // 输出: 1
}

常用方法:

  • release(): 返回该对象的管理的指针, 同时释放所有权.
  • reset(): 释放管理的内存, 同时也可以传递一个新对象的指针.
  • swap(): 交换所管理的对象.
  • get(): 返回对象管理的指针.
  • get_deleter(): 返回析构其管理对象调用的指针.

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ RAII

RAII(Resource Acquisition Is Initialization)即资源获取即初始化, 这里的资源是: 内存, 网络, 文件等对象. 一个资源的生命周期一般包括: 获取资源, 使用资源, 释放资源三个步骤. 不同的编程语言对资源的管理也离不开这三个方面, 其中释放资源应该是最头疼的. C语言采用纯手动回收; Go语言采用的是垃圾回收器; Rust语言采用所有权机制回收(没有垃圾回收器的性能损耗, 却能达到其效果); 而C++即可以手动回收, 也可以通过RAII的方式进行回收. 其中RAII的回收机制和Rust的回收机制有很多相似的地方. 下面看一个例子:

#include <iostream>

class resource {
    int *m_array;

    public:
        resource(const resource&) = delete;
        void operator= (const resource&) = delete;

        resource(int size) {
            m_array = new int[size];
        }

        ~resource() {
            delete []m_array;
        }
};

int main() {
    resource r(100);
}

上面是通过RAII方式构造一个类的典型写法. 整个过程包括以下步骤:

  • 设计一个类对资源进行封装
  • 在构造函数中初始化资源
  • 在析构函数中释放资源

这个过程很简单却无比强大, 我们在使用该类时不需要关心内部资源释放问题, 编译器和作用域规则将接管对象的资源管理细节.

那使用RAII带来什么好处呢?

  • 从繁琐的资源管理中脱身
  • 即使发生异常, 资源也能正确释放
  • 有效的避免空指针引发的bug

RAII典型应用:

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

C++ lock

在多线程编程中被广泛使用, 通过能够保证临界区的数据顺序访问. 在使用的过程中, 最容易现的问题是: 死锁. 幸好C++为我们提供了很好的机制尽量避免死锁的出现.

std::lock_guard 是一个RAII风格的锁, 在离开作用域范围时, 自动释放锁. 下面直接看一个例子:

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <cassert>

static int g_index = 0;
static const int CNT = 1000;
std::mutex g_mutex;

static void _inc(void) {
    for (int i = 0; i < CNT; i++) {
        const std::lock_guard<std::mutex> lock(g_mutex);
        ++g_index;
    }
}

int main(void) {
    std::vector<std::thread> v_th;

    for (int i = 0; i < 10; i++) {
        std::thread t(_inc);
        v_th.push_back(std::move(t));
    }

    for (auto &t : v_th) {
        t.join();
    }

    assert(g_index == 1000 * 10);
    return 0;
}

std::unique_lock是一个功能更全的. 支持延迟锁, 时间锁, 互斥锁, 递归锁, 还能支持转移的所有权等功能. 下面也看一个例子:

#include <cassert>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

static const int CNT = 1000;
static int g_inc_index = 0;
static int g_dec_index = 0;
std::mutex g_inc_mutex;
std::mutex g_dec_mutex;

static void _run(void) {
    for (int i = 0; i < CNT; i++) {
        std::unique_lock<std::mutex> inc_lock(g_inc_mutex, std::defer_lock);
        std::unique_lock<std::mutex> dec_lock(g_dec_mutex, std::defer_lock);

        std::lock(inc_lock, dec_lock);
        ++g_inc_index;
        --g_dec_index;
    }
}

int main(void) {
    std::vector<std::thread> v_th;

    for (int i = 0; i < 10; i++) {
        v_th.emplace_back(_run);
    }

    for (auto &t : v_th) {
        t.join();
    }

    assert(g_inc_index == -g_dec_index);
    return 0;
}

std::scoped_lockstd::lock_guard类似, 不同的是能够同时获取多个. 下面看一个例子:

#include <cassert>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

static int g_inc_index = 0;
static int g_dec_index = 0;
std::mutex g_inc_mutex;
std::mutex g_dec_mutex;

static void _run(const int count) {
    for (int i = 0; i < count; i++) {
        std::scoped_lock lock(g_inc_mutex, g_dec_mutex);
        ++g_inc_index;
        --g_dec_index;
    }
}

int main(void) {
    std::vector<std::thread> v_th;
    const int count = 1000;

    for (int i = 0; i < 10; i++) {
        v_th.emplace_back(_run, count);
    }

    for (auto &t : v_th) {
        t.join();
    }

    assert(g_inc_index == -g_dec_index);
    return 0;
}

如果觉得有帮助,可以扫描右边的微信打赏码支持一下.

参考: