Node.js 中的 addon 是由 C++ 语法制作的,通过 node-gyp 编译的,可以运行在 node 环境下的一种技术,本篇文章记录 C++ 语言的基础(前端JS视角,相通的地方没列举)
一、扫盲
1、C++ 没有运行时环境,代码需要编译成二进制代码才能执行,编译工具比较多,如:Mingw-w64 是 GCC 的 Windows 版本,clang 是 Mac 下的主流编译器
2、window 开发环境搭建:https://www.huaweicloud.com/articles/f5e0b8de8f5659c6389154c429bfb6ab.html;
mac 开发环境搭建:https://zhuanlan.zhihu.com/p/48233069
3、三方依赖管理:https://ccup.github.io/conan-docs-zh/
4、如果使用 VSCode 作为 IDE,则可以安装 Code Runner 扩展工具,可以很方便的运行 C++ 代码
5、C++ 表达式需要用分号作为结束
二、基础语法
1、基本类型
- 有 short,int,long,float,double,char,bool,void 等基本类型,外加 char 的几种变种 wchar_t,char16_t,char32_t
- 可以在部分类型前加 unsigned 表示无符号型,即不包含负数区间
- 不同类型之间互相赋值会发生类型转换,特别的,如果一个 int 和一个 unsigned 类型相加,则会把 int 转为无符号型后再相加,这往往不是希望的:
```cpp
include
using namespace std;
int main(int argc, char const *argv[]) { unsigned u = 10; int i = -42; cout << u + i; // 4294967264 }
- 两个无符号类型相减,也要注意结果是否为负数的情况
- 字面值常量:每种字面值常量都对应一种基本类型,比如 'a' 对应 char,2 对应 int 等;特别的,需要注意字符和字符串的区别,单引号括起来的单个字符称为字符字面量;双引号括起来的字符(0个或多个)称为字符串字面量。
- 字符串字面量在末尾会多一个换行符,其本质是字符数组 char*
```cpp
char *i = "abc";
cout << i;
- 变量的列表初始化(C++11 推荐写法),如果发生精度丢失会报错 ```cpp int a = 1; int b = {1}; // 列表初始化 int c{1}; // 列表初始化 int d(1);
double id = 3.14; int a{id},b={id}; // 报错 int c(id),d=id; // 正常
- 变量定义时没给初始值,会默认初始化。一种特殊情况是定义在函数内部的基本类型变量如果不显示初始化,则不会被默认初始化,未被初始化的变量不能被访问,否则报错
- 变量声明和定义是两回事,一个变量只能被定义一次,但可以被声明多次
```cpp
extern int i; // 声明而非定义一个变量
int j; // 声明并定义
C++ 代码模式是一个头文件+一个源文件,其中头文件负责声明变量或函数或类,源文件负责定义并实现
2、复合类型
引用
int a=12
int &d=a; // 通过 &x 的形式定义一个引用,引用只是一个别名
d=13; // a 变为13
int c=d; // c 为13
int &e; // 错误,引用必须被初始化
指针 ```cpp int t; // 通过 定义一个指针,指向 int 的指针,定义时可以不初始化;
int val = 42; int p = &val; // p 存放的是42在内存中的地址,而不是具体的值42,这个 & 是取地址运算符,不是引用 cout<< p; // 42,利用 * 运算符做解引用操作,可以取到p指向的值
double db; int *p1 = &db; // 错误,指针指向的类型必须一致
int *p2 = nullptr; // 初始值为 空指针
<a name="K3JAm"></a>
### 3、const & auto & decltype
- 当用 const 修饰变量的类型时,那么该变量的值不能被改变;且 const 修饰的变量必须显示初始化
```cpp
const int a = 10;
a = 20; // 错误
const int b; // 错误,需要显示初始化
int b=1;
const int &c=b; // 常量引用
c = 2; // 错误
常量指针,用顶层const表示指针本身是常量,底层const表示其指向的对象是常量
double pi = 3.14;
const double *const pip = π // pip 是一个指向常量的常量指针,pip自身不能变,其指向的对象也不能变
常量表达式 constexpr ```cpp constexpr int mf = 20; constexpr int limit = mf + 1; // 正确 constexpr int sz = size(); // 只有当 size 也是常量表达式时才正确
constexpr int *p = nullptr; // p为常量指针,当 constexpr 修饰指针时,会把指针置为顶层const
- 如果不知道一个值的类型,可以设置为 auto,让编译器计算时自动推导类型
```cpp
auto item = val + val2;
auto sz=0,pi=3.14; // 错误,一条语句只能推导一个类型
const int ci = 0;
auto b = ci; // b是整数,不是常量。auto 会忽略顶层const
const auto b = ci; // b 是常量
auto &g = ci; // g 是常量引用
auto &h = 42; // 错误,设置一个类型为auto引用时,初始值中的常量属性会保留
- 当希望从表达式中获取类型,但是不想用表达式的值作为初始值时,可以用 decltype ```cpp decltype(f()) sum = x; // 使用 f() 的类型,但是不关心 f() 的值
const int ci = 0, &cj = ci; decltype(ci) a = 0; // a 是 const int 类型,decltype保留表达式的原有属性 decltype(cj) b = a; // b 是 const int & 类型
int i =42, p = &i; decltype(p) c; // 错误,c是int & 类型;decltype的表达式如果是解引用,将得到引用类型
decltype((i)) d; // 错误,d是引用,需初始化 decltype(i) d; // 正确,注意双层括号永远得到的是引用
- 可以使用 struct 定义自己的类型
```cpp
struct Book{
std::string bookNo;
unsigned units_sold=0;
double revenue = 0.0;
};
Book book,*booptr;
4、string
标准库类型之一,用于支持可变长字符串操作,使用时需要引用头文件 string.h
#include <string>
using namespace std::string;
// 初始化操作
string s;
string s1(s);
string s2 = s;
string s3("abc"); // s3是 “abc” 副本,除了最后一个空字符外
string s4 = "abc"; // 和上一个等价
string s5(n,'c');
// 常用操作
os<<s;
is>>s;
getline(is,s);
s.empty(); // 返回 size_type 类型
s.size();
s[n];
// 字符串字面量不能直接相加
string s = "hello" + "world"; // 错误
string s = "hello" + string("world");
// 对每个字符的判断,需要借助 cctype 标准库定义的函数
isalnum(a); // a 是字母或是数字
isupper(a); // a是大写字母
toupper(a); // 转为大写
// 范围 for 语句遍历 string
string str("Hello World");
for(auto a:str){
if(isupper(a))
cout<<a<<endl;
}
5、vector
标准库类型之一,用于支持可变长集合操作,使用时需要引用头文件 vector.h
#include <vector>
using namespace std::vector;
// vector 是一个类模版
vector<int> ivec; // ivec 保存 int 类型的对象
vector<vector<int>> file; // 元素是 vector
// 初始化
vector<T> v; // 元素类型为 T 的 vector,会执行默认初始化
vector<T> v2(v);
vector<T> v2 = v;
vector<T> v3(n,val); // n 个 val 填充 v3
vector<T> v4{a,b,c,d};
vector<T> v5 = {a,b,c};
// 常用操作
v.empty();
v.size();
v.push_back(t);
v.pop_back();
v.erase(pos); // 删除迭代器 pos 处的值
v.erase(start,end);
v.clear(); // 清空
v[n];
v = {a,b,c,d};
// 范围 for 语句遍历 vector
vector<int> v{1,2,3,4,5};
for(auto &a:v){
a *=a;
}
for(auto b:v){
cout<<b<<endl;
}
6、迭代器
标准库类型之一,常用在 string 或 vector 的遍历中
auto b = v.begin(),e = v.end();
auto cb = v.cbegin(); ce = v.cend(); // 常量迭代器
7、内置数组
和 vector 很像,但有一点,其 size 不可动态变化
int a[10];
int cnt = 12;
int b[cnt]; // 错误,下标必须是常量表达式
int b[2] = {1,2};
string b[10] = {"abc"};
// 字符数组特殊性
char a3[] = "C++"; // a3 实际包含四个字符,末尾有 "\0"
// 数组不允许拷贝和赋值
int a[]= {1,2};
int a2[] = a; // 错误
a2=a; // 错误
8、C 风格的字符串
这种字符串有个特征,就是以 ‘\0’ 结尾,cstring是操作该字符串的一个标准库
#include <cstring>
char p[] = "abc";
strlen(p);
strcmp(p1,p2);
strcpy(p1,p2);
strcat(p1,p2);
9、显示的类型转换
// static_cast 不能转换掉 const 性质
const char *cp; // cp 是指向常量字符的非常量指针(底层const)
char *p = static_cast<char*>(cp); // 错误
static_cast<string>(cp); // 正确
// const_cast 不能转换原来的类型
const_cast<string>(cp); // 错误
三、函数和类
1、函数
- 当用实参初始化const 形参时,会忽略掉 const,因此定义函数重载时要注意 ```cpp void fun(const int a){}
int b=2; fun(b); // 正确
void fun(int a){} // 错误,重复定义
2、把不会改变的形式参数定义为常量引用是一个好的实践<br />3、如果事先不知道要传递多少个形式参数,可以通过 initializer_list
```cpp
void error_msg(initializer_list il){
for(auto beg=il.begin();beg!=il.end();++beg){
cout<<*beg<<endl;
}
}
4、函数的返回值
// 1、不能是局部对象或局部指针
// 2、可以返回列表初始化
vector<string> process(){
if(xxx)
return {};
if(xxx)
return {"a","b"};
}
// 3、由于数组不能被拷贝,所以函数不能返回数组,但可以返回数组指针
using T = int[10];
T* func(int i);
5、函数实参默认值
string screen(int ht=24,int wid=80,char bg='');
6、使用 inline 构造内联函数,具有更好的性能,一般放在头文件里
inline const string & shortestString(const string&s1,const string &s2);
7、函数指针
函数的类型由返回值和参数共同决定
// 函数类型为 bool(const string &s1,const string &s2)
bool lengthCompare(const string &s1,const string &s2);
// pf 是一个函数指针
bool (*pf)(const string &s1,const string &s2);
// 当把函数名当作值使用时,会自动转换成指针
pf = lengthCompare; // pf 时函数指针
pf = &lengthCompare; // 等价
// decltype 作用于函数时,总是取得函数类型,而非指针类型
2、类
使用关键字 class 定义类,struct 可以定义数据结构,其成员都是 public 的
const 成员函数
// 默认情况下,this 是一个非常量的常量指针,为了使其成为指向常量的常量指针,可以用const
string Sales_data::isbn() const {return this->isbn;}
定义一个类
class Sales_data
{
// 公有函数
public:
// 构造函数
Sales_data() = default; // 默认构造函数
Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {} // 重载,列表初始值
Sales_data(const string &s) : bookNo(s) {} // 重载
Sales_data(istream &);
// 成员函数
string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data &data);
// 私有函数
private:
double avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
string bookNo;
unsigned units_sold;
double revenue;
};
隐式的类类型转换 ```cpp item.combine(string(“abcd”));// 用 string 对象转为 Sales_data
// 只能转换一步 item.combine(“abcd”); // 错误,转了两次 =>string=>Sales_data
// 使用 explict 修饰构造函数可禁止默认转换 explict Sales_data(const string &s) : bookNo(s) {} // 重载 item.combine(string(“abcd”));// 错误
- 字面值常量类
```cpp
// 通过以 constexpr 做构造函数,其成员必须都是字面值
- 静态成员 ```cpp // 1、静态成员是共有的,不是某个实例独有
// 2、静态成员和 this 没什么关系,因此不能声明为 const
// 3、仍可以通过实例或指针或引用访问静态成员
// 4、static 关键字只能出现在类内,因此类外定义static函数时不能再使用 static
// 5、静态成员可以作为默认实参,可以是不完全类型
<a name="yiORv"></a>
## 四、动态内存
- C++内存分为动态内存、静态内存和栈内存,其中动态内存通过 new 和 delete 操作来控制。
- 动态内存的释放是一个不太好管理的操作,为此标准库提供了两种智能指针:shared_ptr,unique_ptr,都定义在 memery 头文件中,用来代替 new delete 操作
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1548284/1630845062093-8c2b5465-a944-4f6f-8cee-09e50173b427.png#clientId=u2f86ae4d-a283-4&from=paste&height=190&id=u529762a8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=379&originWidth=603&originalType=binary&ratio=1&size=120484&status=done&style=none&taskId=u97d2dbcd-288f-4f32-9c2d-d027cf994a2&width=301.5)
- 基本用法
```cpp
#include <memory>
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptra2(ptra); //copy
std::cout << ptra.use_count() << std::endl; // 2
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
std::unique_ptr<int> uptr2 = uptr; //不能赋值
std::unique_ptr<int> uptr2(uptr); //不能拷贝
std::unique_ptr<int> uptr2 = std::move(uptr); // 转换所有权
uptr2.release(); //释放所有权
uptr2.reset(); // 销毁
- 注意点
1、当object第一次创建的时候,就要立刻交由一个shared_ptr管理,其他的shared_ptr和weak_ptr都必须从第一个shared_ptr拷贝或赋值得到。具体来说,就是调用shared_ptr(raw ptr)和make_shared函数。
class Thing;
shared_ptr<Thing> p1(new Thing);
shared_ptr<Thing> p2(make_shared<Thing>());
2、能用裸指针解决问题的情况下,就不要使用智能指针
3、weak_ptr观察着shared_ptr管理的对象,必须从shared_ptr或weak_ptr创建。其唯一正确的使用方法是先用lock()调用提升为shared_ptr,然后使用shared_ptr。如果直接用shared_ptr(weak_ptr)来构造,可能会在weak_ptr已经expire的情况下抛出异常。