C++目前在一些领域处于垄断地位,比如数据库内核、高性能网络代理、基础软件设施 等基本都是C/C++的垄断领域,虽然其他语言也有在做,但是生态、性能等都无法企及,其次C/C++有着丰富的生态,很多高级语言也提供了接口可以对接C/C++ (JNI/CGO等) ,这样你可以很方便的将一些底层C/C++库链接到自己的项目中,避免造轮子!本人学习C++目的是为了看懂别人的代码,因为很多优秀的项目都是C++写的,而非我从事C++相关领域开发!
个人觉得 C++比较难的是 内存管理 + 编译工具 了,其他就是庞大且复杂的语法/模版需要勤加练习和使用,本文主要就是点到为止!
本篇文章会长期更新和补充!
学习环境
个人觉得如果你是一个新手,一定要选一个利于学习的环境,个人比较推荐新手用 clion!其次本文全部都是基于 C++11 走的,目前C++ 版本有 98、14、20 !编译工具用的gcc + cmake,camke学习成本并不是太高,可以看我写的文章: cmake入门!
如果你是c++开发同学最好选择自己公司的编译工具和开发规范!
书籍的话我不介意去看花大量时间去看 C++ Primer 这种书籍,介意掌握一定基础后直接读 effective c++,实践才是硬道理!
从hello world 开始
cpp
复制代码
#include <iostream>
int main() {
std::cout << "Hello"
<< " "
<< "World!" << std::endl;
}
不清楚大家对于上面代码比较好奇的是哪里了?比如说我好奇的是为啥<< 就可以输出了, 为啥还可以 << 实现 append 输出? 对,这个就是我的疑问!
那么带着问题,我们可能要熟悉 C++操作符重载 和 C++面向对象编程! 然后你就可以很好的理解上面代码了!
思考一下是不是等价于下面这个代码了?是不是很容易理解了就!可以把 operator<< 理解为一个方法名! 具体细节下文会讲解!
复制代码
#include <iostream>
int main() {
std::operator<<(std::cout,"Hello").operator<<(" ").operator<<("World!").operator<<(std::endl);
}
内置类型
注意C++很多时候都是跨端开发,所以具体基础类型得看你的系统环境,常见的基础类型你可以直接在 en.cppreference.com/w/cpp/langu… 这里查看 !
char* 和 char[] 和 std::string
本块内容可以先了解一遍,看完本篇内容再回头看一下会理解一些!
字符串在编程中处于一个必不可少的操作,那么C++中提供的 std::string 和 char* 区别在呢了?
简单来说const char* xxx= "字面量" 的性能应该是最高的,因为字面量分配在常量区域,更加安全,但是注意奥不可修改的!
char[]= "字面量" | new char[]{} 分配在栈上或者堆上非常不安全,这种需求直接用 std::vector 或者 std::array 更好!
std::string 在C++11有了移动语意后,性能已经在部分场景优化了很多,进行字符串操作比较多的话介意用这个,别乱用std::string* 。使用 std::string 一般不会涉及到内存安全问题,无非就是多几次拷贝! 如果用指针最好也别用裸指针,别瞎new,可以用智能指针,或者参数[引用]传递!
下面是一个简单的例子,可以参考学习!
cpp
复制代码
#include <cstring>
#include <iostream>
using namespace std;
const char* getStackStr() {
char arr[] = "hello world";
// 不能这么返回,属于不安全的行为,因为arr分配在栈上,你返回了一个栈上的地址,但是这个函数调用这个栈就消亡了,所以不安全!
return arr;
}
const char* getConstStr() {
// 不会有内存安全问题,就是永远指向常量池的一块内存
// 对于这种代码,我们非常推荐用 const char*
const char* arr = "hello world";
return arr;
}
const char* getHeapStr() {
// stack 分配在栈上, 将数据拷贝到返回函数 arr上!
char stack[] = "hello world";
char* arr = new char[strlen(stack) + 1]{};
strcpy(arr, stack);
*arr = 'H';
arr[1] = 'E';
// arr 分配在堆上,我们返回了一个裸指针,用户需要手动释放,不释放有内存安全问题
return arr;
}
// 这里std::string直接分配在堆上, 它的回收取决于 std::unique_ptr 的消亡, 具体有兴趣可以看下智能指针
// 注意: 千万别用函数返回一个裸指针,那么它是非常不安全的,需要手动释放!
std::unique_ptr<std::string> getUniquePtrStr() {
auto str = std::unique_ptr<std::string>(new std::string("hello world."));
str->append(" i am from heap and used unique_ptr.");
return str;
}
// 注意: 这里返回的str实际上进行了一次拷贝,实现在std::string的拷贝构造函数上!
std::string getStdStr() {
std::string str = "hello world.";
str += " i am from stack and used copy constructor.";
return str;
}
int main() {
// a是一个指针,指向常量区, "hello world" 分配在常量区,对于这种申明C++11推荐用 const 标记出来,因为常量区我们程序运行时是无法修改的
char* a = "hello world";
// b是一个指针,指向常量区,"c++" 分配在常量区
// 常量区编译器会优化,也就是说 a 和 b 俩人吧他们的内容都一模一样,那么所以常量只有一份
const char* b = "hello world";
const char* c = "hello world c";
printf("%p\n", a);
printf("%p\n", b);
printf("%p\n", c);
// arr 分配在栈上,当函数调用结束就销毁了!
char arr[] = "1111";
// 乱码!!!
cout << getStackStr() << endl;
// 正常
cout << getConstStr() << endl;
// 常量是不会重复分配内存的,所以下面3个输出结果是一样的!
auto arr1 = getConstStr();
auto arr2 = getConstStr();
printf("%p\n", arr1);
printf("%p\n", arr2);
printf("%p\n", b);
auto arr3 = getHeapStr();
// 正常打印
cout << arr3 << endl;
// 需要手动释放
delete[] arr3;
// std::string 是一个类,也就是说它内存开销非常的高,而且对于大的数据会分配在堆上性能以及效率会差一些!
// 这里本质上调用的是 str的copy constructor函数,属于隐式类型转换!
std::string str = arr1;
cout << str << endl;
printf("%p\n", str.data());
// 业务中如何使用 std::string了,最好使用std:unique_ptr,可以减少内存的拷贝!
// c++ 中一般不推荐return一个复杂的数据结构(因为涉及到拷贝,
// 或者你就用指针,或者C++11引入了移动语意,降低拷贝),而是推荐通过参数把返回变量传递过去,进而减少拷贝!
cout << *getUniquePtrStr() << endl;
cout << getStdStr() << endl;
}
注意点
关于 x++ 和 ++x
首先学过Java/C的同学都知道,x++ 返回的是x+1之前的值, ++x返回的是x+1后的值! 他俩都可以使x加1,但是他俩的返回值不同罢了!
cpp
复制代码
#include <iostream>
using namespace std;
// 实现 x++
int xadd(int& x) {
int tmp = x;
x = x + 1;
return tmp;
}
// 实现 ++x
const int& addx(int& x) {
x = x + 1;
return x;
}
int main() {
int x = 10;
// int tmp = x++;
int tmp = xadd(x);
cout << "x: " << x << ", tmp: " << tmp << endl;
// reset
x = 10;
// tmp = ++x;
tmp = addx(x);
cout << "x: " << x << ", tmp: " << tmp << endl;
tmp = tmp + 1;
cout << "x: " << x << ", tmp: " << tmp << endl;
// 输出:
// x: 11, tmp: 10
// x: 11, tmp: 11
// x: 11, tmp: 12
}
引用 (左值/右值引用)
不介意前期看!
下面是一个简单的例子,可以看到引用的效果
cpp
复制代码
void inc(int& a, int inc) {
a = a + inc;
}
using namespace std;
int main(int argc, char const* argv[]) {
int a = 1;
inc(a, 10);
cout << a << endl;
return 0;
}
// 输出:
// 11
其实上面这个例子属于左值引用,为什么叫左值引用,是因为它只能引用 左值 , 你可以理解为左值 是一个被定义类型的变量,那么它一定可以被取址(因为左引用很多编译器就是用的指针去实现的), 右值则相反,例如字面量; 右值包含纯右值和将亡值(将亡值我个人理解是如果没有使用那么下一步就被回收了,生命到达终点的那种!)
cpp
复制代码
int x = 10;
// x: 是一个变量,其内存分配在栈空间上,为左值,我可以取x的指针,那么x指针指向的就是栈上的某个空间
// 10: 是一个字面量,为右值,如果没有x那么它就和谁也没关系,认为是垃圾(注意右值引用就是要用垃圾,让垃圾生命延续)
char* s= "hello world";
//
具体可以看下面这个例子
cpp
复制代码
#include <iostream>
#include <vector>
using namespace std;
void rvalue(int&& x) {
x = x + 2;
}
void lvalue(int& x) {
x = x + 1;
}
void pointValue(int* x) {
*x = *x + 1;
}
int main() {
int number = 10;
lvalue(number); // number 可以通过指针查找到,是一个已经明确分配的内存!
pointValue(&number); // number可以取指针
// rvalue(number); // 不可以
rvalue(10); // 可以,因为10是一个字面量!
rvalue([] { return 10; }()); // 可以,因为返回值10如果不用就当垃圾销毁了!
// x++ 返回的是右值,额外发生了临时变量拷贝,返回的是拷贝的数字,为右值
// lvalue(number++);
rvalue(number++);
// ++x 返回的是左值,直接在x上自增返回!
lvalue(++number);
// rvalue(++number);
}
有兴趣的可以看文章:
补充: 并不是所有函数返回值都是右值,例如函数可以返回一个左值,例如 ++x
右值引用技巧
- 右值引用可以解决无用的内存拷贝!右值如果你没有使用那么就是无用数据,如果我们利用起来它就是有用的值了!
- 通常我们需要定义 两个构造函数,一个是拷贝构造函数 、 一个是移动构造函数!
类的初始化函数
类的基本的成员函数
这个是C++ 最难的地方,新手做到知道即可,不建议深挖,无底洞一个!
C++ 的类,最基本也会有几个部分组成,就算你定义了一个空的类,那么它也会有(前提你使用了这些操作) ,和Java的有点像!
- default constructor: 默认构造函数
- copy constructor: 拷贝构造函数 (注意: 编译器默认生成的拷贝构造函数是浅拷贝!)
- copy assigment constructor: 拷贝赋值构造函数
- deconstructor: 析构函数 !
- C++11引入了 move constructor (**移动构造函数 **) 、 move assigment constructor(移动赋值构造函数)
补充:
- 默认的拷贝/拷贝赋值构造函数是浅拷贝
- 默认不会生成移动构造函数和移动赋值构造函数
- 移动 和 拷贝的区别在于,移动的本质是a指向b, 拷贝的本质是 a 要拷贝一份b的数据,会有新数据的产生
std::move()可以将一个左值改变成一个右值,在部分场景会用到!- 拷贝构造函数用于 左值 或者 未定义移动构造函数的case
- 参考文章: paul.pub/cpp-value-c…
例如下面 TestStrcut 实际上我重写了这几个函数,大概我们看一下它的调用逻辑吧
cpp
复制代码
#include <iostream>
using namespace std;
class TestStrcut {
public:
TestStrcut() { cout << "invoke default constructor" << endl; }
TestStrcut(const TestStrcut& ts) { cout << "invoke copy constructor" << endl; }
TestStrcut& operator=(const TestStrcut& ts) { // 注意: 这里的&不是取值符号,而是一个引用符号! https://www.cnblogs.com/haruyuki/p/15683592.html
cout << "invoke copy assigment constructor" << endl;
return *this;
}
~TestStrcut() { cout << "invoke deconstructor" << endl; }
};
TestStrcut foo(TestStrcut ts) {
return ts;
}
int main(int argc, char const* argv[]) {
// 省略写法 TestStrcut ts1;
TestStrcut ts1 = TestStrcut();
// 省略写法 TestStrcut ts2(ts1);
// ts2为啥了,因为初始化一个新变量的时候是不会执行 copy assigment
// constructor的,省了一步吧!
TestStrcut ts2 = TestStrcut(ts1);
// ts3这里为啥了? 不直接调用 copy assigment constructor
// 了,因为你是一个新变量,不需要考虑 = 这种操作符! 但是你需要考虑初始化!
// 不然ts1 和 ts2都需要调用 = 操作符
TestStrcut ts3 = ts2;
TestStrcut ts4;
// 这里就做了一个赋值操作,因为ts4已经初始化了,但是要把ts3赋值给ts4.
ts4 = ts3;
cout << "===start===" << endl;
// 函数调用要值传递,所以要copy一下, ts4 copy给了 foo函数的参数1,
// 函数有返回值,返回值也需要拷贝一次;
TestStrcut ts5 = foo(ts4);
cout << "===end===" << endl;
return 0;
}
输出
shell
复制代码
invoke default constructor
invoke copy constructor
invoke copy constructor
invoke default constructor
invoke copy assigment constructor
===start===
invoke copy constructor
invoke copy constructor
invoke deconstructor
===end===
invoke deconstructor
invoke deconstructor
invoke deconstructor
invoke deconstructor
invoke deconstructor
总结:
用到再说!
初始化列表
这里我们要知道一点就是 C++ 类的初始化内置类型(builtin type)是不会自动初始化为0的,但是类类型(非指针类型)的话却会自动调用默认构造函数,具体为啥了,兼容C,不然会很慢,因为假如你要初始化一个类,例如定义了10个内置类型的字段,我需要10次赋值调用才能把10个字段初始化成0,而不初始化只需要开辟固定的内存空间即可,可以大大提高代码运行效率!
cpp
复制代码
#include <iostream>
// struct Info{
// int id;
// long salary;
// };
using namespace std;
class Demo {
public:
int id;
Demo() {
cout << "init demo" << endl;
}
};
class Info {
public:
int id;
long salary;
Demo wrapper;
};
int main() {
// 未使用初始化列表
Info info;
cout << info.id << endl;
cout << info.wrapper.id << endl;
int x;
cout << x << endl;
Info* infop;
cout << infop << endl;
// 使用初始化列表
cout << "======= C++11 初始化列表 " << endl;
Info info1{};
cout << info1.id << endl;
cout << info1.wrapper.id << endl;
int x1{};
cout << x1 << endl;
Info* infop1{};
cout << infop1 << endl;
}
// 输出
// init demo
// 185313075
// 88051808
// 32759
// 0x10b11c010
// ======= C++11 初始化列表
// init demo
// 0
// 0
// 0
// 0x0
类的初始化列表:
类的初始化写法
C++11 就下面这三种写法
- ( expression-list ) 小括号括起来的表达式列表
= expression表达式{ initializer-list }大括号括起来的表达式列表
然后这三种写法大题分为了几大类,这几大类主要是为了区分吧,我个人觉得就是语法上的归类,主要是cpp历史包袱太重了,其次追求高性能,进而分类了很多初始化写法,具体可以看官方文档: en.cppreference.com/w/cpp/langu… !
类的多态
前期先掌握基本语法吧,实际用到的时候再深入学习,类的继承在C++中特别复杂,因为会涉及到模版、类型转换、虚函数、析构函数,注意事项非常多!
继承
下面是一个继承的例子,注意c++是支持多继承的,具体原因自行百度!
cpp
复制代码
#include <iostream>
using namespace std;
class A {
public:
virtual void Print() {
cout << "A::Print" << endl;
}
void BasePrint() {
cout << "A:BasePrint::Print" << endl;
}
};
// 继承A类
class B : public A {
public:
virtual void Print() {
cout << "B::Print" << endl;
}
void BasePrint() {
cout << "B:BasePrint::Print" << endl;
}
};
// 继承A类
class D : public A {
public:
virtual void Print() {
cout << "D::Print" << endl;
}
void BasePrint() {
cout << "D:BasePrint::Print" << endl;
}
};
// 继承B类
class E : public B {
public:
virtual void Print() {
cout << "E::Print" << endl;
}
void BasePrint() {
cout << "E:BasePrint::Print" << endl;
}
};
int main() {
A a;
B b;
E e;
D d;
A* pa = &a;
B* pb = &b;
D* pd = &d;
E* pe = &e;
pa->Print(); // A::Print
pa->BasePrint(); // A:BasePrint::Print
pa = pb;
pa->Print(); // B::Print
pa->BasePrint(); // A:BasePrint::Print
pb->BasePrint(); // B:BasePrint::Print
pa = pd;
pa->Print(); // D::Print
pa->BasePrint(); // A:BasePrint::Print
pd->BasePrint(); // D:BasePrint::Print
pa = pe;
pa->Print(); // E::Print
pa->BasePrint(); // A:BasePrint::Print
pe->BasePrint(); // B:BasePrint::Print
return 0;
}
override 、final
override(重写) 和 overload(重载) 区别在于 override 是继承引入的概念!
这俩修饰词主要是解决继承中重写的问题!
- 类被修饰为 final
cpp
复制代码
class A final {
public:
void func() { cout << "我不想被继承" << endl; };
};
class B : A { // 这里会被编译报错,说A无法被继承!
};
- 方法被修饰为 final
cpp
复制代码
class A {
public:
virtual void func() final { cout << "我不想被继承" << endl; }; // 申明我这个函数无法被继承,注意: final只能修饰virtual函数
};
class B : A {
public:
void func(); // 这里编译报错,无法重写父类方法
};
- 方法修饰为 override
cpp
复制代码
class A {
};
class B : A {
void func() override; // 这里编译报错,重写需要父类有定义!
};
protected
public 和 private其实没多必要介绍, 但是涉及到继承,仅允许我的子类访问那么就需要protected关键词了,区别于Java的protected.
friend
friend (友元)表示外部方法可以访问我的private/protected变量, 正常来说我定义一个一些私有的成员变量,外部函数调用的话,是访问不了的,但是友元函数可以,例如下面这个case:
cpp
复制代码
#include <iostream>
class Data {
friend std::ostream& operator<<(std::ostream& os, const Data& c);
private:
int id{};
std::string name;
};
std::ostream& operator<<(std::ostream& os, const Data& c) {
os << "(Id=" << c.id << ",Name=" << c.name << ")";
return os;
}
int main() {
std::cout << Data{} << std::endl; // 这里会涉及到运算符重载的一些细节,具体可以看本篇文章!
}
指针的一些细节
注意:别瞎new指针, new了地方要么用智能指针自动回收,要么用delete手动回收! 手动new的一定会分配在堆上,所以性能本身就不高!
什么叫指针,可以理解为就是一块内存区域的地址,这个地址就是一个64/32位的无符号整数,可以通过操作这个内存地址进行 获取值(因为指针是有类型的),修改内存等操作!
在C/C++ 语言中,表示指针很简单,例如 int* ptr 表示ptr是一个int类型的指针 或者 一个int数组!
判断指针为空用 nullptr !
cpp
复制代码
int main(int argc, char const* argv[]) {
using namespace std;
int num = 10;
int* ptr; // 表示ptr是一个int类型的指针
ptr = # // 取num的地址
num = *ptr; // 取ptr的值
if (ptr) { // 判断ptr不为空, 也可以与 nullptr 或 NULL 进行比较,我比较推荐与 nullptr 比较,比较直观!
cout << "ptr is not nil" << endl;
}
int* ptr2 = nullptr;
if (!ptr2) { // 判断ptr2为空
cout << "ptr2 is nil" << endl;
}
}
// 输出:
// ptr is not nil
// ptr2 is nil
例子1: 数组与指针
C++/C 中数组和指针最奇妙,原因是 数组 和 指针 基本概念等价,因为两者都是指向内存的首地址,区别在于数组名定义了数组的长度,但是指针没有数组长度的概念,因此我们无法通过一个指针获取数组长度!
类似于下面这个例子, arr 是一个数组,p1、p2是一个数组指针
cpp
复制代码
int main(int argc, char const* argv[]) {
int arr[] = {1, 2, 3, 4, 5};
int* p1 = arr;
int* p2 = &arr[0];
cout << "sizeof(arr)=" << sizeof(arr) << ", sizeof(arr[1])=" << sizeof(arr[1]) << ", sizeof(p1)=" << sizeof(p1) << ", sizeof(p2)=" << sizeof(p2) << endl;
cout << "arr len=" << sizeof(arr) / sizeof(arr[0]) << endl;
cout << "arr=" << arr << ", p1=" << p1 << ", p2=" << p2 << endl;
for (int i = 0; i < 5; i++) {
cout << "i=" << i << ", (arr+i)=" << arr + i << ", (p1+i)=" << p1 + i << ", arr[i]=" << arr[i] << ", *(p1+i)=" << *(p1 + i) << endl;
}
return 0;
}
输出
shell
复制代码
sizeof(arr)=20, sizeof(arr[1])=4, sizeof(p1)=8, sizeof(p2)=8
arr len=5
arr=0x7ff7bd9999f0, p1=0x7ff7bd9999f0, p2=0x7ff7bd9999f0
i=0, (arr+i)=0x7ff7bd9999f0, (p1+i)=0x7ff7bd9999f0, arr[i]=1, *(p1+i)=1
i=1, (arr+i)=0x7ff7bd9999f4, (p1+i)=0x7ff7bd9999f4, arr[i]=2, *(p1+i)=2
i=2, (arr+i)=0x7ff7bd9999f8, (p1+i)=0x7ff7bd9999f8, arr[i]=3, *(p1+i)=3
i=3, (arr+i)=0x7ff7bd9999fc, (p1+i)=0x7ff7bd9999fc, arr[i]=4, *(p1+i)=4
i=4, (arr+i)=0x7ff7bd999a00, (p1+i)=0x7ff7bd999a00, arr[i]=5, *(p1+i)=5
结论:
- 数组、数组指针其实都是 数组的第一个元素对应的内存地址(指针)
- 数组+1 和 指针+1 ,其实不是简单的int+1的操作,而是偏移了类型的长度,原因是 指针是有类型的,且指针默认重载了 + 运算符!
- 数组是可以获取数组的长度的,但是数组指针不可以!
例子2: 数组长度
通常,我们不可能在main函数里写代码,是不是,我们更多都是函数调用,那么问题来了? 函数调用如何安全的操作呢?
cpp
复制代码
int* get_array() {
int* arr = new int[12];
for (int i = 0; i < 12; i++) {
*(arr + i) = i + 1;
}
return arr;
}
int main(int argc, char const* argv[]) {
int* arr = get_array();
for (int i = 0; i < 12; i++) { // 这里无法获取数组指针 arr 的长度
cout << *(arr + i) << endl;
}
return 0;
}
问题: 如何获取arr的长度的呢? 显然是不可以获取的!
例子3: 常量指针
- 常量指针(Constant Pointer),表示的是指针指向的内存(内容)不可以修改,也就是说
*p不可以修改,但是p可以修改
cpp
复制代码
int const* p; // const 修饰的是 *p, *p不可以变(指向的内容),但是p可以变
const int* p; // 写法上没啥区别, 都修饰的是 *p, 我比较推荐这种写法
例子
cpp
复制代码
int main(int argc, char const* argv[]) {
using namespace std;
int x = 10;
int* p2 = new int;
const int* p = &x;
// *p = 10; // 不允许改变 指针指向的值
p = p2; // 允许
cout << "p: " << *p << endl;
return 0;
}
// 输出:
// p: 0
- 指针常量(pointer to a constant:指向常量的指针),表示 p 不可以修改,但是
*p可以修改
cpp
复制代码
int* const p
例子
cpp
复制代码
int main(int argc, char const* argv[]) {
using namespace std;
int x = 10;
int* p2 = new int;
int* const p = &x;
*p = 20; // 允许改变 指针指向的值
// p = p2; // 不允许
cout << "p: " << *p << endl;
return 0;
}
// 输出
// p: 20
- 指向常量的常量指针
cpp
复制代码
const int* const p; // 它兼容了两者的全部优点!
- 总结
大部分case都是使用常量指针,因为指针传递是不安全的,如果我们的目的是不让指针去操作内存,那么我们就用 常量指针,对与指针本身来说就是一个64位的int它变与不变你不用管!
补充一些小点
-
指针到底写在 类型上好
int* p,还是变量上好int *p, 没有正确答案,我是写Go的所以习惯写到类型上!具体可以看 www.zhihu.com/question/52… -
指向成员的指针运算符: (比较难理解,个人感觉实际上就是定义了一个指针 alies )
智能指针
在C++11中存在四种智能指针:std::auto_ptr,std::unique_ptr,std::shared_ptr, std::weak_ptr,
auto_ptr : c++98 中提供了,目前已经不推荐使用了
unique_ptr: 这个对象没有实现拷贝相关的构造函数,所以我们用的时候只能用 std::move 进行移动赋值
shared_ptr: 其实类似于GC语言的对象,他通过引用计数,实现垃圾回收,可以多个对象持有!
weak_ptr: 暂时还没学习
智能指针会涉及到 std::move 和 std::forward 函数相关知识, 有兴趣可以了解下 完美转发和万能引用,以及移动语意!
例子
说实话单想,真想不到啥例子,因为我才刚学习C++,没有太多经验!
cpp
复制代码
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
class A {
public:
~A() { cout << "调用A的析构函数, 我被释放了" << endl; }
private:
public:
int getNum() const { return num; }
void setNum(int d) { A::num = d; }
private:
int num;
};
void printA(std::shared_ptr<A> a) { cout << "num is: " << a->getNum() << endl; }
void handler(std::shared_ptr<A> a) {
static int num = 1;
a->setNum(num);
printA(a);
num = num + 1;
}
int main() {
auto a = unique_ptr<A>(new A());
if (a == nullptr) {
cout << "(1) a is null" << endl;
}
auto b = std::move(a);
// auto c = a; // 报错
if (a == nullptr) {
cout << "(2) a is null" << endl;
}
if (b == nullptr) {
cout << "(3) b is null" << endl;
}
auto aa = std::shared_ptr<A>(new A());
auto bb = aa;
handler(bb);
handler(aa);
}
// 输出
// (2) a is null
// num is: 1
// num is: 2
// 调用A的析构函数, 我被释放了
// 调用A的析构函数, 我被释放了
关键词
const
常量表示不可变的意思,最直接的表达就是,我这个变量初始化后你就不能进行赋值操作了!区别于其他语言,其他语言const不能用于函数的参数申明,但是C++可以,现在很多语言都可以了,主要表达的意思就是 这个参数 不可以做任何修改!
上文实际中讲到了 常量指针 和 指针常量的区别,所以也不太多解释了,const修饰的是const右边的值!
这里主要是介绍一个双重指针,其他疑问可以看这个链接: www.zhihu.com/question/43…!
cpp
复制代码
#include <iostream>
void foo1() {
using namespace std;
int* x = new int(10);
int* const* p = &x; // 表示*p是常量
cout << **p << endl;
**p = 100; // **p允许修改
cout << **p << endl;
// *p = x2; // *p不允许修改!
}
void foo2() {
using namespace std;
const int* x = new int(10);
const int** p = &x; // 表示**p是常量, 因为它也不需要要用常量*x初始化, 不然编译报错!
cout << **p << endl;
*p = new int(11); // *p可以修改
cout << **p << endl;
// **p = 10; // **p不可以修改
}
int main() {
foo1();
foo2();
}
- const 可以修饰方法的返回值
cpp
复制代码
const char* getString() { return "hello"; }
int main() {
auto str = getString();
*(str + 1) = 'a'; // 这里编译报错,只读 str
return 0;
}
- const 修饰方法的参数
cpp
复制代码
void printStr(const char* str) { cout << str << endl; } // 这里无法修改str
int main() {
printStr("1111");
return 0;
}
- const 修饰方法, 表示此方法是一个只读的函数
cpp
复制代码
class F {
private:
int a;
public:
void foo() const { this->a = 1; } // 编译报错,无法修改 this->a !
};
static
这个如果你学过Java,static就再陌生不过了,主要差异在于初始化方式上和生命周期上!
- 面向对象
在面向对象,我们经常会用到一些静态类,赋予这个类一些静态方案和静态变量,那么我们就可以无需初始化这个静态类就可以使用了!
cpp
复制代码
#include <iostream>
using namespace std;
class StaticClass {
private:
static int counter;
public:
static int inc();
};
// must init
int StaticClass::counter = 0;
int StaticClass::inc() {
return ++StaticClass::counter; // 访问静态成员变量,可以 类名::成员变量名,可以用.
}
int main(int argc, const char* argv[]) {
cout << "inc: " << StaticClass::inc() << endl;
cout << "inc: " << StaticClass::inc() << endl;
return 0;
}
- 静态方法和静态变量
注意点就是: 静态变量和全局变量没啥区别,区别在于静态变量、方法不可以被其他文件所访问(可以防止重命名,虽然命名空间可以解决),这个需要特别注意,它有着全局变量的生命周期! 可以参考: zhuanlan.zhihu.com/p/37439983
cpp
复制代码
#include <iostream>
int inc() {
static int sum = 0;
return ++sum;
}
int main(int argc, char const* argv[]) {
std::cout << inc() << std::endl;
std::cout << inc() << std::endl;
return 0;
}
// 输出:
// 1
// 2
extern
后续再补充吧,目前还不太理解!
auto 和 decltype
看这里之前建议先学习模版
auto 实际上是大部分高级语言现在都有的一个功能,就是类型推断,c++11引入auto 原因也是因为模版, 其次更加方便!
decltype 本质上也是类型推断,但是它与 auto 是俩场景,解决不同的问题
cpp
复制代码
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { // 返回类型的后置写法!
using Sum = decltype(t + u);
Sum s = t + u;
s = s + 1;
return s;
}
int main() {
auto num = add(float(1.1), int(1));
cout << num << endl;
return 0;
}
上面代码,如果没有 decltype 很难去实现,如果仅用模版根本无法推断出到底返回类型是啥,可能是int 也可能是 float !
注意:
- decltype 最难的地方还是在于它保留了 左值/右值信息,这个就给编程带来了一定的难度!
- c++14 有更精简的语法,具体可以看c++14语法
using 和 typedef
看这里之前先学习模版
虽然大部分case两者差距不大,using 这里主要解决了一些case 语法过于复杂的问题!
例如 typedef 无法解决模版的问题,只能依赖于类模版去实现!
using 更加方便!
cpp
复制代码
#include <iostream>
#include <list>
template <typename T, template <typename> class MyAlloc = std::allocator>
using MyAllocList = std::list<T, MyAlloc<T>>;
int main() {
auto my_list = MyAllocList<int>{1, 2, 3, 4};
for (auto item : my_list) {
cout << item << endl;
}
return 0;
}
如果用typedef 我们只能定义一个 类
cpp
复制代码
#include <iostream>
#include <list>
template <typename T, template <typename> class MyAlloc = std::allocator>
struct MyAllocList2 {
public:
typedef std::list<T, MyAlloc<T>> type;
};
int main() {
auto my_list_2 = MyAllocList2<int>::type{1, 2, 3, 4};
return 0;
}
操作符重载(运算符重载)
本质上操作符重载就是可以理解为方法的重载,和普通方法没啥差别!但是C++支持将一些 一元/二元/三元的运算符进行重载!
实际上运算符重载是支持 类内重载、类外重载的,两者是等价的!但是有些运算符必须要类内重载,例如 = 、[]、()、-> 等运算符必须类内重载!
这也就是为啥 ostream 的 <<仅仅重载了部分类型,就可以实现输出任意类型了(只要你实现了重载),有别于一些其他语言的实现了,例如Java依赖于Object#ToString继承,Go依赖于接口实现等!运算符重载的好处在于编译器就可以做到检测!
cpp
复制代码
#include <iostream>
using namespace std;
class Complex {
private:
int re, im;
public:
Complex(int re, int im) : re(re), im(im) {
}
// 语法就是 type operator<operator-symbol>(parameter-list)
Complex operator+(const Complex& other) {
return Complex(this->re + other.re, this->im + other.im);
}
void print() {
cout << "re: " << re << ", im: " << im << endl;
}
};
int main(int argc, char const* argv[]) {
Complex a = Complex(1, 1);
Complex b = Complex(2, 2);
Complex c = a + b;
c.print();
return 0;
}
// 输出:
// re: 3, im: 3
lambda
首先lambda 其实在函数式编程很常见,但实际上我个人还是不理解,如果为了更短的代码,我觉得毫无意义,只不过是一个语法糖罢了!
那么什么才是lambda?我觉得函数式编程,一个很强的概念就是(anywhere define function)任意地方都可以定义函数,例如我现在经常写Go,我定义了一个方法,我需要用到某个方法,但是呢这个作用范围我不想放到外面,因为外面也用不到。因此分为了立即执行函数和变量函数
go
复制代码
type Demo struct {
Name *string
}
func foo() {
newDemo := func(v string) *Demo { // newDemo变量 是一个函数类型
return &Demo{
Name: func(v string) *string {
if v == "" {
return nil
}
return &v
}(v), // 立即执行函数
}
}
demo1 := newDemo("1")
demo2 := newDemo("")
fmt.Println(demo1.Name)
fmt.Println(demo2.Name)
}
那么换做C++,我怎么写呢? 是的如此强大的C++完全支持, 哈哈哈哈!注意是C++11 !
cpp
复制代码
struct Demo {
public:
const char* name;
Demo(const char* name) : name(name) {
}
};
void foo() {
auto newDemo = [](const char* name) {
return new Demo([&] {
if (*name == '\0') {
const char * null;
return null;
}
return name;
}());
};
Demo* d1 = newDemo("111");
Demo* d2 = newDemo("");
std::cout << d1->name << std::endl;
std::cout << d2->name << std::endl;
}
基于上面的例子我们大概知道了如何定义一个 变量的类型是函数 , 其次如何定义一个立即执行函数!
- 函数类型
cpp
复制代码
/**
[=]:通过值捕捉所有变量
[&]:通过引用捕捉所有变量
[&x]只通过引用捕捉x,不捕捉其他变量。
[x]只通过值捕捉x,不捕捉其他变量。
[=, &x, &y]默认通过值捕捉,变量x和y例外,这两个变量通过引用捕捉。
[&, x]默认通过引用捕捉,变量x例外,这个变量通过引用捕捉。
[&x, &y]非法,因为标志符不允许重复。
*/
int add1(int x, int y) {
auto lam = [&]() { // [&] 表示引用传递
x = x + 1;
y = y + 1;
return x + y;
};
return lam();
}
int add2(int x, int y) {
auto lam = [=]() { // [=] 表示值传递,不可以做写操作,类似于const属性
// x = x+1; // 不可以操作
// y = y+1; // 不可以操作
return x + y;
};
return lam();
}
int add3(int x, int y) {
// &x表示传递x的引用
// y 表示函数参数
// 类型是: std::function<int(int)>
std::function<int(int)> lam = [&x](int y) {
x = x + 1;
return x + y;
};
return lam(y);
}
- 立即执行函数
cpp
复制代码
int main(int argc, char const* argv[]) {
// lam: 函数类型
std::function<int(int, int)> lam = [](int a, int b) { return a + b; };
std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl;
// 立即执行函数
[] { std::cout << "立即执行函数" << std::endl; }();
return 0;
}
// 输出:
// 10 8
// 立即执行函数
- 函数作为参数传递
cpp
复制代码
std::function<void()> print(std::string str) throw(const char*) {
if (str == "") {
throw "str is empty";
}
return [=] { std::cout << "print: " << str << std::endl; };
}
int main(int argc, char const* argv[]) {
try {
print("")();
} catch (const char* v) {
std::cout << "函数执行失败, 异常信息: " << v << std::endl;
}
print("abc")();
return 0;
}
// 输出:
// 函数执行失败, 异常信息: str is empty
// print: abc
注意点:
- 区别于仿函数,仿函数是重载了
()运算符,仿函数本质上是类,但是C++11引入了std::function也就是 lamdba 简化了仿函数,所以C++11 不再推荐仿函数了! - 区别于函数指针
cpp
复制代码
#include <algorithm>
#include <iostream>
#include <vector>
class NumberPrint {
public:
explicit NumberPrint(int max) : max(max){};
void operator()(int num) const { // 仿函数
if (num < max) {
std::cout << "num: " << num << std::endl;
}
};
private:
int max;
};
void printVector(std::vector<int>&& vector, void (*foo)(int)) { std::for_each(vector.begin(), vector.end(), foo); }
void printNum(int num) { std::cout << "num: " << num << std::endl; }
int main() {
printVector(std::vector<int>{1, 2, 3, 4}, printNum);
auto arr = std::vector<int>{1, 2, 3, 4};
std::for_each(arr.begin(), arr.end(), NumberPrint(3));
}
枚举
C++的枚举继承了C,也就是支持 enum 和 enum class,两者的区别主要是在于作用范围的不同, 例如下面 Child 和 Student 都定义了 Girl 和 Body,如果不是 enum class 的话则会报错!
cpp
复制代码
#include <iostream>
#include <map>
// 允许指定类型
enum class Child : char {
Girl, // 不指定且位置是第一个就是0
Boy = 1,
};
const static std::map<Child, std::string> child_map = {{
Child::Girl,
"Girl",
},
{
Child::Boy,
"Boy",
}};
std::ostream& operator<<(std::ostream& out, const Child& child) { // 重载方法 << 方法
auto kv = child_map.find(child);
if (kv == child_map.end()) {
out << "Unknown[" << int(child) << "]";
return out;
}
out << kv->second;
return out;
}
enum class Student {
Girl,
Boy
};
using namespace std;
int main() {
Child x = Child::Boy;
cout << x << endl;
cout << int(x) << endl;
cout << Child(100) << endl;
}
模版
模板是 C++ 中的泛型编程的基础,实际上C++泛型就是基于模版做的,叫模版原因就是实际上你写的代码就是模版,编译期会根据实际你用到的类型,给你生成对应的代码(函数模版 or 类模版),所以它不会出现Java中的泛型擦除。
日常开发中我们基本上也用不到自己去定义一个模版,基本上都在公共库中存在,所以会用即可!
这里我大概介绍一下大家的困惑,实际上写模版的时候吧你看一个资料它可能写在上面,也有可能写在左侧。其次可能用class也可能用 typename ,那么到底是啥了?
注意: 模版由于其特殊性实际上只能在头文件中定义,主要原因是 涉及到C++的动态/静态链接,假如你模版类/函数 生成到动态链接库引用,头文件只定义申明,那么模版是在编译期生成的代码,导致 链接库就没有对应类型的代码,导致编译失败!
注意:还有一种解决方案就是 显示申明模版,让编译器强制给你生成这个模版类型的实现,但是也有局限性就是 tm 的不能包含全部类型哇!所以你看STL的模版库 全部是 header 头,不过还有一种方案就是 我们只要在头文件中可以拿到模版类型的基础信息就可以了,然后向下传递,参考fmt包!
cpp
复制代码
#include <iostream>
using namespace std;
// 写法1: 比较推荐的写法
template <typename T> T min(T& x, T& y) {
return x < y ? y : x;
}
// 写法2: 模版定义在上面
// template <typename T>
// T min(T& x, T& y) {
// return x < y ? y : x;
// }
// 写法3: 使用class关键词,后续会补充两者区别,也可以直接搜文档,https://liam.page/2018/03/16/keywords-typename-and-class-in-Cxx/
// template <class T> T min(T& x, T& y) {
// return x < y ? y : x;
// }
int main(int argc, char const* argv[]) {
cout << "min(1,2)=" << min(1, 2) << endl;
cout << "min(1.1,2.2)=" << min(1.1, 2.2) << endl;
return 0;
}
- class 与 template 区别 liam.page/2018/03/16/…
- template 可以处理 嵌套依赖类型 !
cpp
复制代码
class Foo {
public:
typedef int bar_type;
};
template <typename T>
class Bar {
public:
typedef typename T::bar_type bar; // 嵌套依赖类型
bar data;
};
- class 可以处理 模板作为模板参数 , 有点像Java的 泛型有界类型参数 本质上就是定义泛型参数的范围
cpp
复制代码
template <typename T, size_t L>
class MyArray {
T arr[L];
public:
MyArray() {
}
};
template <typename T, size_t L, template <typename, size_t> class Arr>
class MyClass {
T t;
Arr<T, L> a;
};
int main(int argc, char const* argv[]) {
auto b = MyClass<int, 10, MyArray>();
return 0;
}
-
模版的一些其他用法
- 函数模版特化:这个类似于方法的重载,你可以理解为模版没有限制类型,但是你可以针对于某个类型去自定义实现
cpp
复制代码
using namespace std;
template <typename T> void print(T t) {
cout << "print: " << t << endl;
}
template <> void print(int t) {
cout << "number: " << t << endl;
}
int main() {
print("hello");
print(1);
return 0;
}
// 输出
// print: hello
// number: 1
STL
STL:(Standard Template Library)叫做C++标准模版库,其实可以理解为C++最核心的部分,很多人望而却步,其实我感觉还好!
主要包含:
- 容器类模板: 基本的数据结构,数组、队列、栈、map、图 等,如果你学习过很多高级语言,那么对于C++这些容器结构我觉得其实不用太投入,只要熟悉几个API就可以了!
cpp
复制代码
// 头文件
#include <vector>
#include <array>
#include <deque>
#include <list>
#include <forward_list>
#include <map>
#include <set>
#include <stack>
- 算法(函数)模板:基本的算法,排序和统计等 , 其实就是一些工具包
cpp
复制代码
// 头文件
#include <algorithm>
- 迭代器类模板:我觉得在Java中很常见,因为你要实现 for each 就需要实现 iterator 接口,其实迭代器类模版也就是这个了!
cpp
复制代码
// 头文件
#include <iterator>
- 总结
cpp
复制代码
#include <iostream>
#include <algorithm> // 算法
#include <iterator> // 迭代器
#include <vector> // 容器
// 找到targetVal位置,并在targetVal前面插入insertVal
// 未找到则在尾部插入
template <typename C, typename V>
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {
// 迭代器
using std::begin;
using std::end;
// 算法
auto it = std::find(begin(container), end(container), targetVal);
container.insert(it, insertVal);
}
int main() {
// 定义容器
auto arr = std::vector<int>{1, 2, 3, 4};
findAndInsert(arr, 4, 2);
// 算法
std::for_each(arr.begin(), arr.end(), [](decltype(*arr.begin()) elem) { cout << elem << endl; });
return 0;
}
预处理器 - 宏
宏本质上就是在预处理阶段把宏替换成对应的代码,可以省去不少代码工作量,其次就是性能更好,不需要函数调用,直接内联到代码中去了!
宏的玩法太高级,很多源码满满的宏,不介意新手去深入了解!只要能看懂就行了!
简单的例子
cpp
复制代码
#include <iostream>
#define product(x) x* x
using namespace std;
int main() {
int x = product((1 + 1)) + 10; // 展开后: (1 + 1)*(1 + 1) + 10
std::cout << "x: " << x << std::endl;
int y = product(1 + 1) + 10; // 展开后: 1 + 1*1 + 1 + 10
std::cout << "y " << y << std::endl;
#ifdef ENABLE_DEBUG
cout << "print debug" << endl;
#endif
}
// 输出:
x: 14
y: 13
class+宏+类名的意义
注意: 这里要是有windows环境的话可以自己体验下!
不清楚大家阅读过c++源码吗,发现开源的代码中基本都有一个 ,那么问题是 PROTOBUF_EXPORT 干啥了?
cpp
复制代码
class PROTOBUF_EXPORT CodedInputStream {
//...
}
实际上你自己写代码没啥问题,定不定义这个宏,你要把代码/ddl提供给别人用windows的开发者来说就有问题了,别人引用你的api需要申明一个 __declspec(dllexport) 宏定义,表示导出这个class,具体可以看 learn.microsoft.com/en-us/cpp/c… 所以说对于跨端开发来说是非常重要的这点!
其次这个东西很多时候可以在编译器层面做手脚,表示特殊标识,反正 大概你知道 windows 下需求这个东东就行了!
cpp
复制代码
#define DllExport __declspec( dllexport )
class DllExport C {
int i;
virtual int func( void ) { return 1; }
};
RTTI
待补充!
多线程
单独补充!
资料
-
godbolt.org/ 一个C++ 转 汇编的工具
-
cppinsights.io/ 可以看到编译器编译后的结果
-
C++学习的一些网站资料(直接去Github找)