头文件
C++和C一样,在源文件的头部需要加入头文件,来告诉预处理器你需要用到什么库。编写方式也和C语言一样,不过基础头文件不是stdlib.h而是iostream,名字划分为io和stream。
stream翻译过来就是“流”的概念,流的概念不是C++特有的,而是Linux下的一个概念,在Linux下一切皆文件,输入输出也是文件,称为输入流输出流,编程语言输入输出的背后就是用标准输入标准输出来实现的,标准输入流默认绑定在键盘输入,标准输出默认绑定在终端窗口,标准输入输出可以通过重定向绑定到文件或者其他上。
#include<iostream> //通用的header file使用扩折号
#include"my_self_header_file.h" //自己编写的header文件使用英语双引号
C++在Linux下的默认include查找路径是/usr/local/include和/usr/local/include,当安装了新的库时,要么将该库的include路径加到C_INCLUDE_PATH,要么在默认路径中加一个软链接指向库的include文件夹。我更推荐第二种,便于管理。
命名空间
C++有非常多的开发库,这些库中有很多算法函数名称是一样的,当同时用到两个不同的功能库中一样名称的函数时应该如何区分是来自哪个空间的呢?为了解决这个问题,C++中定义了命名空间这个概念,当出现上面的问题时,可以在函数的前面加上命名空间的名字,就可以区分了。
#include<iosteam>
using namespace std;
// std 就是基础标准库的命名空间
// using namespace std的意思就是声明我要用到这个命名空间,这样就不需要在变量前加std了
//大型项目最好不要这样直接声明使用整个命名空间,会导致命名空间污染,使用在函数前面加命名空间名称的方式较好。
int main(){
cout<<"hello"<<endl;
// 如果前面没有声明命名空间,那么就要使用下面这行代码来代替
// std::cout<<"hello"<<std::endl;
return 0;
}
当我们在参与一个大型项目的时候,我们经常在不同的模块中可能会实现两个名称相同的函数,为了解决这个问题,我们可以进行自定义命名空间,下面代码为如何自定义一个命名空间
#include<iostream>
namespace mynamespace{
void func(){};
}
int main(){
mynamespace::func();
return 0;
}
命名空间不仅仅对函数有用,也对变量有用。但是有一些常见变量我们可能在一个程序中就会用到很多个同名的,比如temp这个名字,使用频率非常高,难道要都给他们定义一个命名空间吗?C++中的变量声明和函数声明都是有自己的使用空间限制的,出了这个空间声明就无效了,一般都是以{}为一个空间,英文叫一个scope,可以根据大小分为global scope和local scope
其实namespace也是一个scope
#include<iostream>
int globalnum; //这是一个全局变量,就是说在全局通用
int main{
std::cout<<globalnum<<std::endl;
for(int i=0;i<10;i++){
std::cout<<globalnum<<std::endl; //可以
}
std::cout<<i<<std::endl; //会报错,因为已经出了scope
return 0;
}
面向对象编程
C++是一个较为多元的编程语言,可以满足多种编程范式,C++可以进行面向对象编程。
- 类class,是抽象的,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。
- 对象/实例,是实实在在的,是类的实体化。
面向对象编程比较符合人的直觉,但是想要写出好的面向对象代码不能只靠直觉,面向对象有23种常见的设计模式,是进行面向对象编程所必学的。
#include<iostream>
#include<string>
class Person{
public:
Person(const string namestr=0); //构造函数
Person(const Person& originalperson);//拷贝函数 构造函数,拷贝函数,析构函数,默认不用写函数返回类型
Person& operator= (const Person& originalperson); //拷贝赋值函数
inline std::string getName(){return name;}
~Person(){}; //析构函数
private:
std::string name;
int height;
int age;
};
inline Person::Person(const std::string namestr)
{
if(namestr.size())
{
name = std::string(namestr);
}else
{
name = std::string();
}
}
int main()
{
Person bighammer("王大锤");
std::cout<<bighammer.getName()<<std::endl;
return 0;
}
上面实现了一个最简单的类。麻雀虽小,五脏俱全。一个类最少应该包括以下几部分:
- 普通构造函数
- 析构函数
- 拷贝构造
- 赋值运算重载
C++中初始化对象可以使用malloc和free,也可以使用new和delete,new和delete会调用类的构造函数和析构函数,可以避免空指针的存在。【二者初始化的对象所在位置也不同,new时在自由存储区为对象动态分配内存,malloc是在堆栈上分配空间】
必须实现拷贝构造的原因是如果不实现拷贝构造,C++会自动为你实现一个完全复制的拷贝构造,如果你的类里面有指针,指针也会被复制过去,这样就可能导致两个对象指向同一个数据,引发bug。
C++标准库 & 模板编程
C++标准库是对C标准库的扩充,对C的库稍作修改,并增加了面向对象以及模板编程的内容。
C++另一种常用编程范式就是模板编程或者说泛型编程,泛型编程的思想是逻辑代码独立于任何特定类型,能够有效实现代码复用。
模板可以分为函数模板和类模板,模板参数又可以分为类型模板参数和非类型模板参数,两两结合,一共四种可能:
template<typename T> // 类型函数模板
T funcadd(T a, T b)
{
T addhe = a + b;
return addhe;
}
template<int a, int b> // 非类型函数模板,浮点数不能作为非类型模板参数
int funcaddv2()
{
int addhe = a + b;
return addhe;
}
template<typename T> //类型类模板
class MyClass
{
private:
T* temp;
void func();
}
template<typename T>
void MyClass<T>::func(){}
template<int id> // 非类型类模板
class MyClass2
{
private:
int number=id;
}
//不过非类型一般都是更类型一块儿用,例如array类
template<typename T,int size>
class Array
{
private:
T arr[size];
void func2();
}
template<typename T,int size>
void func2(){}
不管是函数模板还是类模板,都是在用到时才能生成一个实例,如果多文件编程,为了防止产生多个实例,可以在类模板的文件中显式实例化:
template MyClass<float> //进行显式实例化
extern template MyClass<float> //其他文件中就只能进行外部实例化声明
这里的实例化是指将模板实例化,生成对应类型的代码,而不是面向对象编程中的将一个类实例化成一个对象。
现代C++中还支持模板特化和偏特化,如果一个模板对于某些特定的类型需要进行单独编写代码,就叫特化,如果所有类型都要进行特殊编写代码,就叫全特化,如果局部类型进行特化,称为偏特化:
template<>
struct TC<int, int>
{
TC()
{
cout << "TC<int,int>特化版本的构造函数" << endl;
}
// 这里可以对特化的版本做单独的处理
}
struct TC<int, double>
{
TC()
{
cout << "TC<int,double>特化版本的构造函数" << endl;
}
// 这里可以对特化的版本做单独的处理
}
template <typename U> // 偏特化,3个参数绑定2个。
struct TCP<int, U, double>
{
TCP()
{
cout << "TCP偏特化版本的构造函数" << endl;
}
void functest(){
cout << "TCP偏特化版本"<< endl;
}
}
除此之外,还有模板参数范围特化,可变参数模板,模板模板参数等等,自行了解。
STL标准模板类就是一个模板编程的集大成者,标准模板库STL主要由以下几部分组成:
- 容器类:数据类型,其中又分为序列容器和非序列容器
- 算法类:适用于容器类的常见算法的实现,如sort,search
- 迭代器:适用于容器类的迭代器
- 适配器:可变容器,迭代器或者函数对象接口的一种组件
- 函数对象:用类型包装函数
- 分配器:进行内存分配
现代C++
随着C++的发展,引入了不少新的特性以及编程范式,目前大多将C++11以后的版本,称为现代C++。C++11更新的特性非常多,网上都说C++11是C++跨时代的发展,可以参考:《深入理解C++11:C++11新特性解析与应用》这本书,讲的很好,对于了解C++11很有帮助。
函数式编程
函数编程的思想是:程序是函数定义的表达式树,这些函数将一个值映射到另一个值,而不是传统的状态机模型意义下的命令式语句。
C++11中引入了lambda匿名函数,使得C++进行函数式编程更为容易,函数式编程现在很火,这两年发展迅猛的Rust也是函数式编程的忠实拥护,基本上目前大部分编程语言都支持函数式编程,匿名函数,闭包,是函数式编程中的重要概念。
C++工具 & 功能库
C++因为其标准委员会只负责制定标准,不负责实现,以及C++实现的不统一,导致其工具也非常多。MSVC和GCC和Clang各自都有很多配套的工具,所以学习C++不光要会语法以及特性,还要学习配套的工具和相应领域的工具库,
工具:
Cmake:用于自动化配置编译,生成makefile
ninja:构建系统,也是用于构建cpp项目的
gdb:调试工具
gprof:性能测试工具,来自GNU Binutils,其他的可以不会,这个一定要会用。
valgrind:内存泄漏检测工具
功能库
计算机视觉:OpenCV
矩阵计算:Eigen
客户端开发:Qt
深度学习推理引擎:Onnxruntime
后续想到什么再慢慢缝缝补补吧,C++太大了,太繁杂了,令人眼花缭乱