Cpp 函数API设计
整理一下关于现代化的C++函数API设计的笔记,
我们关注如何设计可读性好,使用安全的函数接口:设计函数接口的名称、参数类型、返回值等。
部分参考现代化的
API 设计指南
基本原则
函数API的设计要满足如下的原则:
向用户传达清晰的语义,保证接口的可读性,避免因为语义不清晰导致用户的错误调用;
向编译器提供更多的信息,尽可能让错误的使用在编译时就被发现,由编译器发出警告或直接导致编译失败。
如果需要考虑:
跨编译器使用:混用不同编译器编译得到的库
跨语言使用:混用C语言和C++,或者将当前的C++库提供给其它语言调用(例如Python)
那么必须保证使用extern "C"提供C语言形式的接口。
函数名称
保持统一的函数名称风格:大驼峰或使用下划线,例如 123void get_date();void SetDate();
函数名称通常使用动词加名称的形式,避免提供名称相似的函数接口,尤其是不要出现仅有大小写不同的函数接口。
匈牙利命名法已经过时了,尤其对于C++这种变量类型非常明确的语言,我们可以借助IDE或clangd等工具很方便地检查变量类型。
函数参数
函数 ...
Cpp const* 关键词
C++关于常量提供了很多形如const*的关键词,在语义上涉及到运行期的常量约束和编译期的初始化这两个性质,这几个关键词的含义相当类似,有必要整理一下。
概述
C++陆续引入了下面几个关键词:
第一代:const,从C语言中继承,在C++中一直都存在
第二代:constexpr,C++11引入
第三代:consteval和constinit,C++20引入
其中const继承自C语言的,其它几个则是C++独有的关键词,关于它们的用法仍然在迅速发展中,因为C++很想发掘出编译期计算的巨大潜力,但是历史包袱导致它一直干不好。
我们重点关注的是编译期计算,可以使用如下的两种方式来检查:
第一种方式利用在数组的定义,数组长度需要提供一个编译期的值,因为普通变量的初始化始终发生在运行期,直接使用一个变量作为数组长度是无法通过编译的。(有时确实可以通过编译,但那属于编译器的扩展功能,不是语法标准规定的用法)
第二种方式是使用C++专门提供的static_assert关键字来进行静态断言(将其称为编译期断言更合适,与C语言中assert所代表的运行期断言相对应),它会在编译期判定一个编译期表达 ...
C语言 定义和声明
定义和声明
在 C
语言中,定义和声明是两个极其容易混淆的概念,定义是明确告诉编译器完整的信息,而声明只是给编译器打一个“白条”,具有如下特点:
声明对应的定义可以暂时并不提供,定义可能在源文件的后面,也可能在其它源文件或编译单元中。
前置声明可以让我们在正式定义之前就使用它,这对于解决循环依赖是必要的。
同样的声明可以重复多次提供,但是定义至多只有一次,否则会报错符号重定义。
如果编译器在链接所有编译单元时,如果需要使用某个具体实现,但是没有找到声明对应的定义——对应的“白条”没有被兑现,就会报错符号未定义。
如果只是提供了一个声明,但是没有实际使用它,那么编译器会直接将其忽略掉,此时即使没有提供对应的定义也是允许的,因为压根不需要。
我们主要考虑变量的声明/定义和函数的声明/定义:
变量:
变量声明:变量声明只是告诉编译器变量的名称和类型,但不分配内存。
变量定义:变量定义是在编译过程中分配内存和值并为变量指定名称的操作。
函数:
函数声明:函数声明(也称为函数原型)包含函数的返回类型、参数列表和函数名,但没有函数体。它只是告诉编译器有这个函数存在,但是并不提供实现。
...
C语言 关于变量和函数的笔记
概述
C语言中的变量可以大致分为4种:
(非静态)局部变量
静态局部变量
(非静态)全局变量
静态全局变量
变量的类别由定义的位置和修饰词决定,例如 12345678int s_global = 10; // 全局变量static int s_global_static = 20; // 静态全局变量void fun(){ int s = 100; // 局部变量 static int s_local_static = 200; // 静态局部变量 // ...}
变量的类别决定了它们具有完全不同的性质。
与变量不同,C语言中的函数大致只需要分成两种:
普通函数
静态函数
函数的类别取决于是否使用了static修饰,不同类别的函数也具有不同的性质。
下面会从不同的角度对变量和函数的类别进行理解。
进程的内存分区
在启动一个可执行程序时,系统会创建一个进程并为其分配一个内存空间,其中包括进程执行所涉及的所有指令和数据,
大致分为如下几个区域:
文本区(Text
Segment):存放程序的具体代码指令(包括函数),这块区域在程序运行期间是只 ...
Cpp 异常处理
C++提供了一套复杂的异常处理机制,虽然很多第三方库都禁止使用异常,但是至少在标准库中广泛采用了异常机制,
因此学习一下相关的语法是有必要的,至于在编程实践中是否采用异常,仍然是值得讨论的。
简单示例
从一个简单的例子开始,这里调用的函数processInput对于非法的输入参数会抛出异常,在main函数中调用时使用try-catch语句块捕获异常进行处理。
123456789101112131415161718192021222324252627#include <iostream>#include <stdexcept>void processInput(int value) { if (value == 0) { throw std::invalid_argument("Input cannot be zero!"); } if (value < 0) { throw std::out_of_range("Input cannot be negative!") ...
Cpp和Python相互调用
记录一下在Cpp程序和Python脚本中相互调用的方法。
Cpp调用Python
在C++程序中调用Python解释器,包括执行简单Python语句(字符串形式),以及执行整个py脚本(文件形式)。
执行简单命令
最简单的例子:调用Python解释器,输出HelloWorld,C++源文件如下
main.cpp12345678910111213141516#ifdef _DEBUG#undef _DEBUG#include <Python.h>#define _DEBUG 1#else#include <Python.h>#endifint main() { Py_Initialize(); PyRun_SimpleString("print('Hello, world! (from Python)')"); Py_Finalize(); return 0;}
注意在导入Python.h时进行了一些处理,因为我们的电脑中通常只有Release版本的Python库,并没有Debug ...
C/Cpp 输出语句的变迁
C语言
printf
这是C语言提供的输出语句,示例如下 1234567#include <stdio.h>int main() { int number = 42; printf("The answer is %d.\n", number); return 0;}
printf支持不定参数,占位符采用%d等。
C++
std::cout
这是C++20之前提供的基于面向对象和流输出的方式,示例如下
1234567#include <iostream>int main() { int number = 42; std::cout << "The answer is " << number << ".\n"; return 0;}
流式输出在简单的情况下比较好用,但是一旦我们需要复杂的格式化输出,这就变得非常繁琐了。
std::cout的输出是带缓冲的,可以手动调用cout.flush()清空 ...
C语言 结构体笔记
关于C语言中的结构体的语法,这是C++自定义类型的基础。
C++的类提供了非常丰富的功能,但是也会尽量兼容C语言的结构体。
简单示例
下面是针对学生信息的结构体的使用示例,包括了结构体对象的定义和读写,以及通过指针传递给函数。
123456789101112131415161718192021222324252627282930313233343536373839#include <stdio.h>#include <string.h>struct Person { char name[50]; int age; double height;};void show(struct Person *person) { printf("Name: %s\n", person->name); printf("Age: %d\n", person->age); printf("Height: %.2f\n", person->he ...
Cpp new/delete 语句
new和delete是C++中最基础且重要的申请和释放堆内存的语法,以它为线索可以引出C++堆内存管理的相关内容,它也是智能指针所需要的基础知识,值得整理一下。
基本使用
内置类型
new和delete最基础的用法是被设计用来替代C语言中的malloc和free的,分配堆内存来构造指定类型的对象,返回对应类型的指针,例如
12int *p = new int; // uninitializeddelete p;
这两个语句都是非常危险的:
最简单的new语句对内置数据类型不会对内存进行初始化,我们得到值是随机的;(Debug模式下可能内存被清空,改成Release模式可以看到随机值)
delete语句不会在释放内存之后将指针置空,指针会变成空悬指针。
我们可以用下面的new语句进行初始化 12345double *p1 = new double; // uninitializeddouble *p2 = new double(); // 0double *p3 = new double{}; // 0double *p4 = new ...
Cpp 显式加载动态库
在 C++ 中,加载动态库通常有两种方式:显式加载和隐式加载。
隐式加载比较简单,只需要设置链接选项即可自动进行,本文主要学习一下显式加载。
概述
在 C++ 中,显式加载和隐式加载动态库的基本介绍如下:
隐式加载是最常用的方式,通常的C++标准库等系统中的库都是采用隐式加载的,程序在编译时需要添加-l选项链接到动态库。在程序启用时,系统会自动查找并加载对应的动态库。
隐式加载的优势是代码简单,不需要在代码中处理加载动态库的各种细节。但缺点是要求在编译时动态库也要参与链接,在编译时和运行时都需要保证动态库是可以找到并且使用的,编译时无法找到则编译失败,运行时无法找到和使用则程序无法启动。
显式加载则是一种更灵活的方式,我们可以在代码中精准地控制加载和卸载动态库的所有细节。
优势是程序在编译链接时完全不需要动态库的参与,程序在运行时可以根据需要有选择性地进行加载或卸载动态库,即使在运行时对某个动态库的加载失败也不会导致程序中止。
基于显式加载我们可以实现动态库的热更新:在使用动态库的程序保持运行的情况下,更新对应的动态库。
这对于一些大型的商业服务是非常普遍的需求,他们不希望因为某些 ...