C++是如何工作的
cpp
#include <iostream>
int main() {
std::cout << "Hello World" << std::endl;
std::cin.get();
}
首先是预处理,会将iostream文件的内容拷贝到当前文件 ,这就是头文件,main函数是程序的入口,程序会从这里逐行执行。操作符实际是函数,"Hello World"就是这个函数的参数,会将"Hello World"推送到cout流里面然后打印到终端,再推送一个行结束符。
项目中头文件不会编译,仅编译cpp文件,因为头文件的内容在预处理的时候就包含进来了。
每一个cpp文件被编译成了object file(目标文件).obj 通过链接link把所有的obj文件合并成了exe文件
链接器可操作的最小元素是一个简单的目标文件,我们见到的所有应用程序,小到自己实现的hello world程序,大到复杂的比如浏览器,都是链接器将一个个所需要用到的目标文件汇集起来最终形成了非常复杂的应用程序
C++编译器如何工作
C++中没有所谓的文件,文件只是提供给编译器源代码的一种方式,如果你创建一个.h的文件,编译器就会把它当作头文件处理。文件没有意义
include文件:预处理器打开这个文件,阅读所有内容,粘贴到你写的文件中
C++链接器如何工作的
链接的主要焦点是找到每个符号和函数在哪,并连接。点击编译的话,链接不会发生。build或者运行会链接。
学会区分错误信息中的链接错误和编译错误
如果用static修饰函数,意味着这个函数只被声明在这个翻译单元中,若这个cpp文件没有调用,那么build将不会得到链接错误。
cpp
void Log(const char* message);
static int M(int a, int b) {
Log("hi");
return a * b;
} // 另一个cpp文件中没有Log函数
当你头文件定义了一个函数,并在两个cpp文件引入,会报错
解决办法:
1.把函数定义为static,这样在链接时,其只能是文件的内部函数,所以两个文件都有自己版本的相同函数。
2.把函数定义为inline,获取函数体,把函数调用替换为函数体
3.将这个函数移动到一个翻译单元,因为上述问题是因为这个函数列入了两个翻译单元。再将头文件的函数改写为函数声明。所以头文件只声明,是为了link!
bool类型的存储
bool按理说只需要1 bit 来存储,但是当计算机在寻址的时候,我们只能寻址字节,所以为了访问bool类型变量,分配的仍然是一个字节内存。所以bool占一个字节的内存 。
并且除了0是false,其他都是1
内联函数
直接执行函数内语句,不会有函数入栈这种操作,减少函数调用带来的开销。(函数声明和定义前面都要加inline)
某些情况编译器不会内联编译:
1.不能存在任何形式的循环语句
2.不能存在过多条件判断
3.函数体不能庞大
4.不能对函数进行取地址操作
C++头文件
#progma once 可以阻止单个头文件被多次包含,并转换为单个翻译单元
#include 工作原理就是复制粘贴
cpp
#ifndef _Test //会检查 _Test 是否定义了,没定义会在编译中包含下列代码
#define _Test
...
#endif
一般用 #progma once 比较简单一点
双引号和尖括号的区别
双引号查找头文件路径的顺序为:
- 先在当前源文件所在的工作目录中进行查找
- 编译器设置的头文件查找路径
- 系统文件目录查找
尖括号:
- 编译器设置的头文件查找路径
- 系统文件目录查找
如果这个头文件在解决方案中的某个地方,或许是另一个项目,那就用引号。
如果它是一个完全的外部依赖或者外部的库,不和我实际解决方案一起编译。就用尖括号,表示它是外部的。
C++预编译头文件
对于比较大型的工程,往往编译时间会很久,通过使用PCH,把那些不经常发生改动的头文件都预先编译出来,就可以大大节省编译时间。
缺点:会减弱文件间的关联性
举个例子,原本我有一个cpp,包含了#include <windows.h>,然后我把这些自己不会改的api的头文件放到PCH里,那么之后我再看这个代码,我就不知道这个cpp具体包含了哪些头文件了,我只知道它用到了PCH,但是不能一眼就看出来它包含了头文件windows.h
创建pch:
-
创建一个简单的源文件A.cpp,和头文件H.h
代码:
cpp------------ #pragma once #include<vector> #include<map> #include<set> #include<windows.h> ------------- #include "H.h" int main() {}
-
tools -> options -> projects and solutions -> VC++priject setting 下设置build timing 为 yes,就能得到构建时长
-
添加对应pch.cpp文件,并把H.h包含进pch文件,右键,在C/C++中的Precomplied Header设置为create
-
项目的属性界面,在C/C++中的Precomplied Header设置为Use
这次再build用时就大大减少
C++namespace
如果我们想要两个print函数,但另一个已经定义在了库里。主要是避免命名冲突。
using namespace xxx;使用时尽量限制在一个小的作用域下,不要在头文件中使用。
cpp
namespace apple {
void print(const char* text) {
std::cout << text << endl;
}
}
namespace orange {
void print(const char* text) {
std::cout << text << endl;
}
}
namespace fun { //定义一个名称空间
int num = 1;
}
using fun::num;
int main() {
apple::print("y");
using namespace std; //就是从std空间导入所有东西
using apple::print; //引入apple名称空间的print函数
std::cout << num << std::endl;
namespace a = apple;
}
名称空间也可以嵌套:using namepace apple::funcitons;
using 编译指令如果出现同名会用就近原则
C++其他
extern "C" 修饰的代码会按照C语言的方式编译,放在声明上
cpp
extern "C" void fun(){}
extern "C" {
void fun() {}
void fun(int v){}
}
extern "C" {
#include "xxx.h"
}
C和C++混合开发用,他们编译规则不同
写在.h文件上
cpp
#ifdef __cplusplus
extern "C" {
#endif
int add (int, int) {}
int sub(int, int){}
#ifdef __cplusplus
}
#endif
函数重载的原理:编译器采用了 name decoration(名称修饰),name mangling(名称矫正)
默认参数:
函数调用省略了实参时自动使用的值
注意:
1.如果参数有了默认值,右边的参数必须都有默认值
2.函数声明和定义都需要写上默认值
静态局部变量
静态局部变量
1.代码位置:在代码块{}定义的变量
2.存储空间:全局区
3.作用范围:离他最近的{}内
4.生命周期:整个程序进程
不初始化则值为0,不同作用域同名变量采用就近原则
c
void fun() {
static int num = 10; // 只有第一次运行才会赋值为10,运行三次结果为:11 12 13
num++;
cout << num << endl;
}
C++中,编译器会在编译器分配内存后,在全局区域(当前静态局部变量的地址)附近同样分配一块空间,进行记录变量是否已经进行过初始化。之所以说附近是根据编译器不同,处理方式不同导致的。
全局变量、文件域的静态变量和类的静态成员变量 在main执行之前 的静态初始化过程中分配内存并初始化;局部静态变量 (一般为函数内的静态变量)在第一次使用时分配内存并初始化。
C++左值右值与std::move
右值:取不到地址的表达式
左值:能取到地址的表达式
常量对象为代表的左值不能在赋值语句的左边
const int a = 1; a = 3;
某些表达式求值结果是对象,但是是右值
可通过这样办法来返回左值引用
cpp
int& GetValue() { //返回左值引用
static int a = 10;
return a;
}
void SetValue(int& value) {}
void PrintName(std::string&& name){} // 右值引用,只能传递临时对象
int main() {
SetValue(10); //不能把右值给左值引用,但是可修改SetValue(const int& value){}
const int& value = 10;// 编译器临时声明了一个变量,所以在函数传值的时候,会加const
}
所以可以知道它们是否是右值,就不用担心它们是否活着、完整、拷贝,可以简单的使用它们的资源,因为它们只是暂时的。
move语义
我们在进行传参的时候,String类拷贝了两次,显然进行两次堆分配。
cpp
#include <iostream>
class String { //此类仅作为例子
public:
String() {}
String(const char* string) {
std::cout << "created\n";
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other) {
std::cout << "copied\n";
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String() {
std::cout << "deleted\n";
delete[] m_Data;
}
uint32_t m_Size;
char* m_Data;
};
class Entity {
public:
Entity() {}
Entity(const String& name) : m_Name(name) {
}
private:
String m_Name;
};
int main() {
Entity("yy");
return 0;
}
运行结果为:
created
copied
deleted
deleted
它在主函数中进行了初始化,堆分配了一次,传给了Entity构造函数中,又需要进行一次复制(产生堆分配),一共两次,有没有什么办法让把第一次的堆分配的内容直接移动到那。
cpp
#include <iostream>
#include <utility>
class String {
public:
String() {}
String(const char* string) {
std::cout << "created\n";
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other) {
std::cout << "copied\n";
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other) noexcept { //不应抛出异常
std::cout << "moved\n";
m_Size = other.m_Size;
m_Data = other.m_Data; // 此时它们的data都指向了同一块内存区域
other.m_Data = nullptr; // 析构时,会销毁的是nullpter
other.m_Size = 0;
}
// 移动赋值构造
String& operator=(String&& other) noexcept {
std::cout << "moved\n";
if (this != &other) {
// 先删除自己的数据
delete[] m_Data;
m_Size = other.m_Size;
m_Data = other.m_Data; // 此时它们的data都指向了同一块内存区域
other.m_Data = nullptr; // 析构时,会销毁的是nullpter
other.m_Size = 0;
}
return *this;
}
~String() {
std::cout << "deleted\n";
delete[] m_Data;
}
uint32_t m_Size;
char* m_Data;
};
class Entity {
public:
Entity() {}
Entity(const String& name) : m_Name(name) {
}
Entity(String&& name) : m_Name(static_cast<String&&>(name)) { //注意这里必须显示的转化为一个临时对象
}
private:
String m_Name;
};
int main() {
Entity("yyyyyyyyyy");
return 0;
}
std::move
std::move函数可以将左值转换为右值引用而避免拷贝构造。
上面的代码可以修改为
cpp
#include <utility>
Entity(String&& name) : m_Name(static_cast<String&&>(name)) {
}
---->
Entity(String&& name) : m_Name(std::move(name)) {
}// 把name转换为右值,调用移动构造
-------------------
String string = "hi";
String string2 = std::move(string); // 这里构造了一个新字符串s2,使用的是一个临时的值,
string2 = std::move(string); // 这是赋值运算符 string2.operator=(std::move(string));
C++三/五法则
- 拷贝构造函数
- 拷贝赋值函数
- 析构函数
- 移动构造
- 移动赋值运算符
如果需要析构函数,则一定需要拷贝构造和拷贝复制运算符
为支持移动语义,增加了移动构造和移动赋值运算符+
右值引用与万能引用
在 C++11 中, &&
既可以表示右值引用,又能表示万能引用
右值引用用于延长临时(将亡)变量的生命周期,常用于移动构造、移动赋值等场景,避免冗余复制操作带来的性能损耗
值得益于 RVO (返回值优化),很多情况下我们没有必要使用右值引用来优化代码,比如以下例子中, GetClazz
函数将在返回的地址上直接初始化,所以对局部变量 c
使用 move 是多余的,还会产生一次移动的多余开销
cpp
struct Clazz {};
Clazz GetClazz() {
Clazz c;
return std::move(c); // Bad
return c; // Good, RVO
}
int main() {
Clazz &&c1 = GetClazz(); // Not necessary
Clazz c2 = GetClazz(); // Good
}
sort排序
第三个参数是一个比较函数对象,若传入的第一个参数排在前面的话,返回true。
cpp
std::sort(values.begin(), values.end(), [](int a, int b) {
return a > b;
});
参数计算过程
cpp
void Print(int a, int b) {
cout << a << b << a + b << endl;
}
int main() {
int value = 0;
Print(value++, value++);
}
这样的结果是未定义行为,取决于编译器,但是在C++17中说了,他们必须一个接一个完成,不能同时计算。
C++持续集成(CI)
C++静态分析
C++类型双关
用来在C++中绕过类型系统,虽然类型是由编译器强制执行的,但是我们可以直接访问内存,我们可以把一段同样的内存,当作double类型或者是class类型等。实质就是把该类型作为指针,然后转换为另一个指针
cpp
int main() {
int a = 50;
double value = a;// 隐式转换,此时a和value内存中的值不同
double value = *(double*)&a; //这就是类型双关,此时内存中的值相同,这个例子中,会把int的后4个字节一起复制给了value变量,这是不好的
如果你不想拷贝,而是引用
double& value = *(double*)&a; //这样就会直接访问int内存,并且修改,这样做很危险
}
如果是一个空的结构体,至少是一个字节,因为我们需要对这段内存进行寻址。但是,如果这个结构体只有两个数,可以看作一个数组
cpp
struct Entity {
int x, y;
}
int main() {
Entity e = {5, 8};
int * position = (int*)&e;
cout << position[0] << " " << position[1];
}
---------------------
struct Entity {
int x, y;
int* GetPositon() {
return &x; //没做任何复制
}
}
int main() {
Entity e = {5, 8};
int * position = e.GetPositon(); //栈上的数据是连续的
}
C++类型转换
可能会让类型转换更可靠,因为它会做编译时检查,而dynamic_cast会做运行时检查
static_cast :用于类层次结构中基类和派生类之间指针或引用的转换。上行转换是安全的,下行转换由于没有动态类型检查,所以不安全。也可以用于基本数据类型的转换,安全性由开发人员保证。static_cast<double>(a);
dynamic_cast :延继承层次结构进行的强制类型转换。基类转派生类,派生类转基类。常用来做验证是否能转换 ,上行时和static_cast效果一样。下行转换有类型检查功能,更安全。若指针转换失败,则返回空指针,若引用转换失败,则抛出异常。
能做到这一点是因为它存储了运行时类型信息(RTTI),我么你可以在设置中关掉
cpp
Java中的
if(actuallyEnemy instanceof Plyaer) {
}
等于CPP中的
Player p0 = dynamic_cast<Player*>(actuallyEnemy);
if(p0) {
...
}
reinterpret_cast :非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。上文类型双关中能做的所有事情都能用这个来转换
const_cast:用于移除类型的const、volatile和__unaligned属性。常量指针被转换成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然引用原来的对象。
C++堆与栈
栈区 --编译器自动分配释放,主要存放函数的参数值,局部变量值等;
堆区 --由程序员分配释放;
全局区 (静态区) --存放全局变量和静态变量;程序结束时由系统释放
常量区 --存放常量值,如字符串常量,程序结束时由系统释放;
代码区 --存放函数体的二进制代码
尽量进行栈分配,除非它的生命周期比作用域更长
进行堆分配时,可能需要检查空闲列表、请求内存,记录,所以这就是比栈慢的原因
cpp
//main.cpp
int a=0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[]="abc"; //栈
char *p2; //栈
char *p3="123456"; //123456\0在常量区,p3在栈上。
static int c=0; //全局(静态)初始化区
p1 = (char*)malloc(10);
strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
}
C++宏
#ifdef 和 #if 等预处理指令配合
可以在项目属性中的C/C++的预处理器中设置预处理器定义。
release下 同理
cpp
#ifdef PR_DEBUG == 1 // 查看是否定义了该宏
#define LOG(x) std::cout << 123 << std::endl
#elif defined(PR_RELEASE)
#define LOG(x) //相当于把LOG换成空
#endif
如果要查看多个宏是否定义过,可使用下面的预处理指令
cpp
#if defined(_WIN32) || defined(WIN32)
// 如果是Windows系统则会编译此段代码
OutputDebugString("this is a Windows log");
#endif
#ifdef
之后的宏只要定义过就会满足条件,而#if
则会看后面的宏的内容是否为真了。
cpp
#define ENABLE_LOG 1
#if ENABLE_LOG
trace("when enabled then print this log")
#endif
防止重复包含头文件
cpp
#ifndef __SYSTEM_API_H__
#define __SYSTEM_API_H__
// 头文件的内容
...
#endif
反斜杠能转移回车
cpp
#define MAIN int main() \
{\
std: :cin.get();\
}
C++ 指针
指针就是地址,指针类型不重要,只是存储内存地址的整数
指针所取内容的宽度由指针变量所指向的类型长度决定
指针变量+1 的跨度由指针变量所指类型的大小决定
cpp
int a[2][3] = { 1,2,4,5,6,8 };
printf("a(0行首地址)=%d\n", a);
printf("a+1(1行首地址)=%d\n\n", a + 1);
printf("&a(整个数组的地址)=%d\n", &a);
printf("&a+1(跨越了整个数组,指向最后一个元素的下一地址)=%d\n", &a + 1);
printf("\n&a[1][2]=%d\n", &a[1][2]);
使用指针需要注意什么?
- 定义指针时,先初始化为NULL。
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
- 不要忘记为数组和动态内存赋初值。
- 避免数字或指针的下标越界,特别要当心发生"多1"或者"少1"操作
- 动态内存的申请与释放必须配对,防止内存泄漏
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止"野指针"
指针的引用传递
先看这个例子
cpp
#include <iostream>
using namespace std;
void test(int* k) {
int x = 99;
k = &x;
}
int main() {
int i = 10;
int *p = &i;
printf("%d\n", *p); // 10
test(p);
printf("%d\n", *p); // 10
system("pause");
return 0;
}
再看指针的引用传递例子
cpp
#include <iostream>
using namespace std;
void test(int*& k) { // 指针引用传递
int x = 99;
k = &x;
}
int main() {
int i=10;
int *p = &i;
printf("%d\n", *p); // 10
test(p);
printf("%d\n", *p); // 99
system("pause");
return 0;
}
智能指针
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
C++11 引入了 3 个智能指针类型:
std::unique_ptr<T>
:独占资源所有权的指针。std::shared_ptr<T>
:共享资源所有权的指针。非线程安全std::weak_ptr<T>
:共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期
std::unique_ptr
保证同一时间内只有一个智能指针可以指向该对象,可以使用 std::unique_ptr 对资源进行管理------离开 unique_ptr 对象的作用域时,会自动释放资源。
使用 std::unique_ptr 自动管理内存
cpp
{
std::unique_ptr<int> uptr = std::make_unique<int>(200); // 对于避免资源泄露(例如"以new创建对象后因为发生异常而忘记调用delete")有用。
// 离开 uptr 的作用域的时候自动释放内存
}
注:如果确实想要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后 还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。
(额外:boost库的boost::scoped_ptr也是一个独占性智能指针,但是它不允许转移所有权,从始而终都只对一个资源负责,它更安全谨慎,但是应用的范围也更狭窄。)
cpp
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
std::shared_ptr
C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法 ,记录当前内存资源被多少个智能指针引用。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时通过make_shared函数或者通过构造函数传入普通指针。
cpp
{
std::shared_ptr<int> sptr = std::make_shared<int>(200);
assert(sptr.use_count() == 1); // 此时引用计数为 1
{
std::shared_ptr<int> sptr1 = sptr;
assert(sptr.get() == sptr1.get());
assert(sptr.use_count() == 2); // sptr 和 sptr1 共享资源,引用计数为 2
}
assert(sptr.use_count() == 1); // sptr1 已经释放
}
// use_count 为 0 时自动释放内存
指向数组
cpp
{
// C++20 才支持 std::make_shared<int[]>
// std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
std::shared_ptr<int[]> sptr(new int[10]);
}
std::weak_ptr
weak_ptr指针是弱指针,指向对象的时候,不增加引用计数。它主要是为了避免shared_ptr循环引用的问题。当shared_ptr循环引用的时候,计数不能清零,因此内存不能被正确释放。
cpp
std::weak_ptr<Entity> weakEntity = sharedEntity;
函数指针
函数当然只是cpu指令,但是当我们编译代码时,他就在二进制文件(可执行文件),对函数取地址就像是说去获取那些cpu指令的内存地址
cpp
void Hello() {
cout << " hi" << endl;
}
int main() {
void(*yy)(); // yy是名字,这就是函数指针的类型(返回void,没有参数)
yy = Hello; //传函数地址
auto yy2 = Hello(); // 常用auto来使用函数指针
yy();
//也可以用typedef和using
typedef void(*yyFunction)();
yyFunction function = Hello;
function();
}
--------------------------带参数的函数如下
void Hello(int a) {
cout << " hi" << endl;
}
int main() {
typedef void(*yyFunction)(int);
yyFunction function = Hello;
function();
}
例子:我们对vector进行遍历时,可能需要做某些操作,这时可以传入函数指针
cpp
void ForEach(const std::vector<int>& values, void(*func)(int)) {
for (int value : values) func(value);
} // 其中函数指针类型改写为: const std::function<void(int)>& func) 要包含functional 头文件,这样才能传lambda
int main() {
std::vector<int> values = { 1, 2, 3, 4 };
ForEach(values, [](int value) { // 可以传函数指针或者lambda
std::cout << value << std::endl;
});
}
std::function是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行 。特别适合作为回调函数 使用。
std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(如:函数指针这类可调用实体,是类型不安全的)。
std::bind
std::bind可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
- 将可调用对象和其参数绑定成一个仿函数;
- 只绑定部分参数,减少可调用对象传入的参数。
cpp
#include <iostream>
#include <functional>
class A {
public:
void fun_3(int k,int m) {
std::cout << "print: k = "<< k << ", m = " << m << std::endl;
}
};
void fun_1(int x,int y,int z) {
std::cout << "print: x = " << x << ", y = " << y << ", z = " << z << std::endl;
}
void fun_2(int &a,int &b) {
++a;
++b;
std::cout << "print: a = " << a << ", b = " << b << std::endl;
}
int main(int argc, char * argv[]) {
//f1的类型为 function<void(int, int, int)>
auto f1 = std::bind(fun_1, 1, 2, 3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
f1(); //print: x=1,y=2,z=3
auto f2 = std::bind(fun_1, std::placeholders::_1, std::placeholders::_2, 3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
f2(1, 2); //print: x=1,y=2,z=3
auto f3 = std::bind(fun_1, std::placeholders::_2, std::placeholders::_1, 3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
//注意: f2 和 f3 的区别。
f3(1, 2); //print: x=2,y=1,z=3
int m = 2;
int n = 3;
auto f4 = std::bind(fun_2, std::placeholders::_1, n); //表示绑定fun_2的第二个参数为n, fun_2的第一个参数由调用f4的第一个参数(_1)指定。
f4(m); //print: a=3,b=4
std::cout << "m = " << m << std::endl; //m=3 说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m
std::cout << "n = " << n << std::endl; //n=3 说明:bind对于预先绑定的函数参数是通过值传递的,如n
A a;
//f5的类型为 function<void(int, int)>
auto f5 = std::bind(&A::fun_3, &a, std::placeholders::_1, std::placeholders::_2); //使用auto关键字
f5(10, 20); //调用a.fun_3(10,20),print: k=10,m=20
std::function<void(int,int)> fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);
fc(10, 20); //调用a.fun_3(10,20) print: k=10,m=20
return 0;
}
- 预绑定的参数是以值传递 的形式,不预绑定的参数要用std::placeholders(占位符)的形式占位,从_1开始,依次递增,是以引用传递的形式;
- std::placeholders表示新的可调用对象的第几个参数,而且与原函数的该占位符所在位置的进行匹配;
- bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址,这是因为对象的成员函数需要有this指针。并且编译器不会将对象的成员函数隐式转换成函数指针,需要通过&手动转换;
- std::bind的返回值是可调用实体,可以直接赋给std::function。
lambda
本质是一种叫做匿名函数,只要有函数指针,你就能使用lambda。
cpp
std::vector<int> values{1, 5, 4, 3, 2};
auto it = std::find_if(value.begin(), values.end(), [](int value){
return value > 3;
});
std::cout << *it << endl;
int a = 1;
auto lambda = [=](int value) { // 值传递,引用传递用&
std:: cout << a << endl;
};
C++构造与析构
当其他类对象作为本类成员的时候
构造顺序 :先调用父类然后是数据成员的构造,再调用本类的
析构顺序:和构造顺序相反
虚析构与纯虚析构
在父类使用虚析构可以解决不调用子类的析构函数的问题。(多态中)也就是说多态中子类有堆区数据,父类释放时无法释放子类的堆区数据而导致内存泄露的问题。
纯虚析构(需要有声明和实现),需要在类外实现
virtual ~Animal() = 0;
Animal::~Animal() {
}
C++new
C语言动态内存分配
malloc(n)
1.不会为该内存命名,会放回内存块的首字节地址
2.返回类型为 void 指针,相当于通用指针
3.存在分配失败的可能性,返回 NULL
free§
free() 不能释放通过其他方式分配的内存(如声明一个数组)
c
char* ch = (char*)malloc(sizeof(char) * size1);
int* arr = (int*)malloc(sizeof(int) * size2);
struct S* s = (struct S*)malloc(sizeof(stuctt S) * size3);
int *p = new int[10];
delete []p;
C++引用
并不占用内存,没有真正的存储空间。类似变量的别名
cpp
// 数组的引用
int arr[3] = {1, 2, 3};
int (&rarr)[3] = arr;
常量引用
左值:可以被引用的数据对象如:变量、数组元素、结构体变量成员、引用
cpp
int &b = 10; // 错误,非 常量引用的初始值必须为左值
const int& b = 10; // 编译器优化,会生成一个临时变量值为10
如果用引用传参时,又不想修改原变量,就用常量引用(加const),好处是能避免无意修改数据且能处理const和非const的实参
使用引用
1.能修改函数外部数据
2.通过传递引用而不是整个数据,没有了副本的拷贝,提高了速度
原则:
1.对于使用传参的值而不做修改的函数。
a.如内置基本数据类型或者小型结构体用值传递 。
b.如果数据是数组 ,使用指针,指针声明为const指针 。
c.如果数据很大,如大型结构体 ,使用const指针或者const引用 。
d.如果数据是类对象 ,使用const引用 。
2.对于要修改的函数
a.若是内置数据类型用指针
b.如果数据是数组,只能用指针
c.如果是结构体,使用引用或指针
d.如果数据是类对象 ,使用引用
引用作为函数返回值
不要返回局部变量的引用,避免函数结束时,其内存单元不再存在
VS2022
输出目录配置为:$(SolutionDir)bin\$(Platform)\$(Configuration)
中间目录配置为:$(SolutionDir)bin\intermediates$(Platform)\$(Configuration)
条件与操作断点
在断点处右键
C++静态
类或结构体外的static,意味着链接将只是在内部,它只能对你定义它的翻译单元可见,链接器不会在翻译单元的作用域外来寻找符号定义。
类或结构体内的static,这个变量被所有的实例共享内存。
extern 意味着关键字会在外部翻译单元中寻找变量,但是找不到其他翻译单元中被类或结构体外的static修饰的变量
static 修饰函数也是和变量差不多
但是尽量别用全局变量!要让函数和变量标记为静态的,除非真的需要跨翻译单元链接。
类和结构体中的静态
如果某些数据你想在一个类的所有实例中共享,你可以把这个变量或者方法声明为静态。静态方法不能访问非静态变量。见类背后在C++工作的原理
静态局部变量的生存期相当于整个程序的生存期,但是作用域只有函数内部
函数作用域的 static 和 类作用域的 static 之间没啥太大区别,生存期相同,但是类作用域中,类的任何东西都能访问这个静态变量
静态数据成员不能在类中初始化,一般在类外和main()函数之前初始化,缺省时初始化为0。
cpp
struct A{
static const int i0;//正确
static const int i1=2;//正确
static const std::string str2;//正确
static const std::string str3="你太baby辣";//错误:只允许static const int有类内初始值
static const float f0=3f;//错误:只允许static const int 类型在类内定义
static constexpr int i2=4;//正确,static constexpr必须是类内初始值
static constexpr int i3;//错误
static constexpr std::string str0;//错误:没有类内初始值
static constexpr std::string str1="c++";//正确
};
//类内定义的static数据成员必须在类外再次定义一次
const int A::i0=1;
const int A::i1;
const std::string A::str2="000";
const std::string A::str3;
const float A::f0;
constexpr int A::i2;
constexpr int A::i3=5;
constexpr std::string A::str0="456";
constexpr std::string A::str1;
C++枚举
cpp
enum colors {red, orange, yellow};
colors a = red;
a = colors(0); // 强转,类型为 colors
int b = 1 + red; // 枚举类型自动提升为 int
enum bits {one = 1, tow = 2, for = 4};
enum bigstep {first, second = 10, third}; // first 默认 0,后面没有初始化的比前面的枚举值大 1
enum {zero, nu = 0, one, nn}; // 可以创建值相同的枚举量,且不能当做类型,只能使用其中的枚举值
枚举变量的值只能取枚举常量表中所列的值,就是整型数的一个子集。
枚举变量占用内存的大小与整型数相同。
枚举变量只能参与赋值和关系运算以及输出操作,参与运算时用其本身的整数值。
C++继承
继承中的对象模型
类中的成员方法和成员变量是分开存储的。对象中,只保存了成员变量的信息
子类将父类中所有成员都继承了,包括私有的。
继承中的构造和析构
创建子类对象时,会先调用父类构造函数(默认构造函数),再调用数据成员的构造函数,最后调用子类构造函数,析构的顺序和构造相反。
通过子类访问父类的同名非静态成员,需要加上作用域
如果子类出现了和父类同名的成员函数,子类的成员函数会隐藏父类的所有同名的成员函数。子类重定义父类的成员函数,如果想调用父类同名的成员函数,必须加上作用域。
cpp
s.Base::func();
// s.func(10);
s.Base::func(10);
继承中静态同名成员的处理
访问父类的静态成员方法,需要加上作用域
继承默认是私有继承
class 子类: 继承方式 父类1, 继承方式 父类2...;
如果两个父类中出现了同名的成员,需要加上作用域进行区分
菱形继承和虚继承
如果有两个子类继承了同一个父类,又有一个类同时继承这两个子类,这种继承成为菱形继承。(访问成员要加作用域)
继承来自父类的数据有两份,浪费了内存。sw.Singer::m_Age = 20;
解决方案:虚继承 class Singer: virtual public Person {};
虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
只有唯一的成员,通过保存vbptr,指向了虚基类表。这个表保存了当前获取唯一的数据的偏移量
C++ 支持编译时多态(静态多态)和运行时多态(动态动态),运算符重载和函数重载就是静态多态,派生类和虚函数实现运行时多态。
静态多态和动态多态区别是函数地址是早绑定(静态联编 )还是晚绑定(动态联编)
父类的引用或指针指向了子类的实例。
虚函数采用的就是动态联编,虚函数表记录了虚函数的函数入口地址。当子类重写父类的虚函数后,子类的虚函数表入口地址发生了覆盖(重写)。
动态多态实现分三步:
- 子类重写父类的虚函数(类内实现)
- 父类指针指向子类对象(类外实现)
- 用该指针调用子类虚函数(类外实现)
C++多态
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
cpp
class Shape {
protected:
int width, height;
public:
Shape(int a = 0, int b = 0) {
width = a;
height = b;
}
int area() {
cout << "Parent class area :" <<endl;
return 0;
}
};
class Triangle: public Shape{
public:
Triangle(int a = 0, int b = 0) : Shape(a, b) { }
int area () {
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
int main( )
{
Shape *shape;
Triangle tri(10,5);
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area(); // Parent class area :
return 0;
}
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态 ,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接 ,或后期绑定。
纯虚函数
当类中有了纯虚函数,这个类属于抽象类,无法实例化。子类如果继承,必须重写纯虚函数,否则子类也是抽象类
我们可以把基类中的虚函数 area() 改写如下:virtual int area() = 0;
虚析构与纯虚析构
在父类使用虚析构可以解决不调用子类的析构函数的问题。(多态中)
纯虚析构(需要有声明和实现),需要在类外实现
virtual ~Animal() = 0;
Animal::~Animal() {
}
C++字符串
必须 const char* name = "yangyu"; 里面的值不能改变,如果想修改,用字符数组
cpp
const char* name = u8"yangyu"; utf8
const wchar_t* name2 = L"yangyu"; 几个字节由编译器决定,win是2 linux是4
const char16_t* name3 = u"yangyu"; utf16
const char32_t* name4 = U"yangyu"; utf32
std::u32string name0 = U"yangyu"s + U"hi";
char arr[3][10] = {"123", "456", "789"};
// 数组是再开辟一个空间,然后把静态存储区的内容复制过来
char *p[3] = {"123", "456", "789"}; // 数组,三个指针,都指向了一个字符串
// 指针指向的静态存储区的,不能修改
字符串的值拥有保存在内存的只读区域
C++const
放在类方法名之后就不能修改类成员变量
cpp
class Entity {
private:
int m_x, m_y;
public:
int GetX() {
return m_x;
}
void SetX(int x) {
m_x = x;
}
};
void PrintEntity(const Entity& e) {
cout << e.GetX() << endl; //这里报错,因为无法保证GetX()不会写入Entity,应该在GetX()后加const修饰
}
int main() {
const Person p2(30); // 不能调用普通函数,能调用常函数。常对象不能修改内容,可以修改mutable修饰的成员变量
}
总是标记自己的函数为const如果它们没有修改类或者不应该修改类,否则有常量引用的情况下不能用这个方法
如果你这个函数确实修改了类,但是你又想标记为const,你可以在被修改的变量处用mutable修饰
lamda中也会用到mutable
cpp
1. const int a; //指的是a是一个常量,不允许修改。
2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a; //同 const int *a;
4. int *const a; //a指针所指向的内存地址不变,即a不变
5. const int *const a; //都不变,即(*a)不变,a也不变
constexpr 能够让其返回值在编译器就可用
C++auto
cpp
vector<string> strings;
strings.push_back("123");
strings.push_back("456");
for(vector<string>::iterator it = strings.begin(); it != strings.end(); it++) {
cout << *it << endl;
} // 这个巨大类型 vector<string>::iterator 可以写成auto
如果类型很长,但是就是诸如int, string 这样的不要使用auto
当进入到更加复杂代码块,包含模板,可能就需要使用aoto
C++union
cpp
union Token{
char cval;
int ival;
double dval;
};
联合可以为其成员指定public、protected和private等访问权限,默认其成员的访问权限为public。
联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足:
-
大小足够容纳最宽的成员
-
大小能被其包含的所有基本数据类型的大小所整除。
C++初始化成员列表
cpp
class Entity {
private:
int m_x;
std::string m_name;
public:
Entity() : m_x(0), m_name("yangyu") {} //顺序要和定义一致
}
这样做的目的是可读性
若不这样做(在构造函数中手动赋值)m_name就被创建了2次,一个是默认构造,一个有参构造。
C++隐式转换和explicit关键字
cpp
#include <iostream>
class Entity {
public:
std::string m_name;
int m_age;
Entity(const std::string& name) : m_name(name), m_age(22) {}
Entity(int age) : m_name("yy"), m_age(age) {}
};
int main() {
Entity b = std::string("yangyu");
Entity a = 10;
return 0;
}
但是尽量避免用隐式转换
explicit关键字放在构造函数前面意味着没有隐式转换
C++运算符重载
运算符重载
operatorop(argument_list)
cpp
Time operator+(const Time &t) { //放在类中
Time time;
time.hours hours + t.hours;
time.minutes = minutes + t.minutes;
return time;
}
Time operator+(const Time &t1, const Time &t2) { // 全局实现运算符重载
...
}
Time t3 = t1 + t2; //本质是t1.operator+(t2)
-------------------------------------------------
// 重载左移运算符
ostream& operator<<(ostream& out, const Person& p) { // 全局实现
out << p.a << " " << p.b << endl;
return out;
}
// 如果a和n是私有的,需要声明为友元
// 如果 cout << p << endl; 这里endl会报错, 因为cout << p; 返回的是void,所以返回值改为ostream&
--------------------------------------------------
// 重载自增 ++
class MyInt {
friend ostream& operator<<(ostream& out, const MyInt m); // 不传引用,因为后置++返回的是一个临时对象
public:
MyInt(){ num = 0;}
// 前置++
MyInt& operator++() {
num++;
return *this;
}
// 后置++ 这里的占位就说明这个是后置
MyInt operator++(int) { // 让它重新拷贝,因为temp是局部变量
// 先记录原来的值
MyInt tmp = *this;
num++;
return temp;
}
private:
int num;
}
--------------------------------------------------
// 指针运算符重载
// 如果类里面在堆里面有内容,并且要用到赋值运算符,那么他只会浅拷贝,这就需要重载=
只能通过成员函数重载的运算符:=、()、[]、->
全局函数配合友元:<<、>>
C++this 指针
指向调用成员函数的对象,谁调用方法,this就指向谁
C++拷贝构造函数
其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。
cpp
class Person {
//...
//拷贝构造函数
Person(const Person & b) {
age = b.age;
}
}
使用条件
-
一个对象作为函数参数,以值传递的方式传入函数体;
cppvoid fun(Person c) { ... } int main() { fun(A); }
-
一个对象作为函数返回值,以值传递的方式从函数返回;
cppPerson fun() { Person tmp(022); return tmp; } int main() { fun(); }
-
一个对象用于给另外一个对象进行初始化
深拷贝与浅拷贝
默认拷贝构造函数
编译器会给我们自动产生一个拷贝构造函数,这就是"默认拷贝构造函数",这个构造函数很简单,仅仅使用"老对象"的数据成员的值对"新对象"的数据成员进行赋值
但是
cpp
class Rect {
public:
Rect() {
count++;
}
~Rect() {
count--;
}
static int getCount() {
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main() {
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
输出的结果是1 1,因为拷贝构造函数没有处理静态数据成员,所以需要重新编写拷贝构造函数!
浅拷贝
指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。
cpp
class Person {
public:
Person() {
p = new int(100);
}
~Person() {
delete p;
}
private:
int *p;
};
int main() {
Rect p1;
Rect p2(p1); // 导致两个对象指针都指向了同一块内存区域
return 0;
}
深拷贝
在"深拷贝"的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。
当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
细节问题
1.为什么拷贝构造函数必须是引用传递,不能是值传递?
是为了防止递归引用 。当一个对象需要以值传递 时,编译器会生成代码调用它的拷贝构造函数以生成一个副本。如果类A的拷贝构造函数值传递了一个类A对象作为参数的话,而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导致又一次调用类A的拷贝构造函数,这就是一个无限递归。
2.参数传递中,对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象) ;如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
3.以下函数哪个是拷贝构造函数,为什么?
cpp
X::X(const X&); //拷贝构造函数
X::X(X);
X::X(X&, int a=1); //拷贝构造函数
X::X(X&, int a=1, int b=2); //拷贝构造函数
解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
C++ 单例模式
- 构造函数和析构函数为private 类型,目的禁止外部构造和析构
- 拷贝构造和赋值构造函数为private 类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
- 类里有个获取实例的静态函数,可以全局访问
推荐写法:
C++的局部静态变量唯一性,可以确保例子中s对象的唯一性,线程同步,以及静态对象间的依赖关系等问题。
(1) 静态局部变量在静态存储区内分配存储单元。在程序整个运行期间都不释放。
(2) 为静态局部变量赋初值是在编译时进行值的,即只赋初值一次,在程序运行时它已有初值。
(3) 如果在定义局部变量时不赋初值的话,对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。
(4) 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的,也就是说,在其他函数中它是"不可见"的。
cpp
class Singleton {
private: Singleton() {}
Singleton(const Singleton &) = delete;
Singleton(const Singleton &&) = delete;
Singleton &operator=(const Singleton &) = delete;
public: static Singleton &getInstance() {
static Singleton s;
return s;
}
};
常规写法:
cpp
#include <iostream>
#include <mutex>
class SingleInstance {
public:
static SingleInstance* GetIntance() {
if (m_instance != nullptr) {
m_Mutex.lock();
if (m_instance != nullptr) { // 确保不会加锁期间多个线程同时进入
m_instance = new SingleInstance();
}
m_Mutex.unlock();
}
return m_instance;
}
private:
SingleInstance();
SingleInstance(const SingleInstance& instance);
const SingleInstance& operator=(const SingleInstance& instance); // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
static SingleInstance* m_instance;
static std::mutex m_Mutex;
};
SingleInstance* SingleInstance::m_instance = nullptr;
std::mutex SingleInstance::m_Mutex;
静态成员:
- 下例中的Singleton::instance 保存在程序全局的静态数据区 ,instance初始化的时机是在程序的main()函数执行前。
- 假设有SingletonB::instance,与Singleton::instance类似定义,也是静态类成员变量。SingletonB::instance和Singleton::instance的初始化顺序是未定义的。
- 如果Singleton::instance 的初始化在SingletonB::instance之前,而Singleton的构造函数中恰好需要引用到SIngleonB::instance,就很可能会出现一些未定义的行为。
cpp
#include <iostream>
class Singleton {
private: static Singleton instance;
public: static Singleton &getInstance() {
return instance;
}
};
Singleton Singleton::instance;
int main() {
Singleton &s = Singleton::getInstance();
return 0;
}
C++动态数组
cpp
// 动态数组进行内存分配的格式为new T[size],size可以不是常量表达式
int s = 10;
int* arr1 = new int[s]; //未初始化
int* arr2 = new int[s](); //默认的初始化;
int* arr3 = new int[s]{1, 2}; ,
string* arr4 = new string[s]{"aa", "bb","cc", "dd", string(2, 'e') };
delete arr1[];// 最后一个元素被最先释放,第一个元素最后一个被释放
delete arr2[];
delete arr3[];
delete arr4[];
多维数组
cpp
int MAX_NUM = 10;
int COL_NUM = 5, ROW_NUM = 3;
double ***Arr3D = new double **[MAX_NUM];
for (int i = 0; i < MAX_NUM; i++) { // 先依次声明维度的大小,然后从最低维度开始申请内存
Arr3D[i] = new double *[ROW_NUM];
for (int j = 0; j < ROW_NUM; j++) {
Arr3D[i][j] = new double[COL_NUM];
}
}
for (int i = 0; i < MAX_NUM; i++) { // 多维动态数组的释放是从最低维度开始的
for (int j = 0; j < ROW_NUM; j++) {
delete[] Arr3D[i][j];
}
delete[] Arr3D[i];
}
delete[] Arr3D;
函数与多维数组
cpp
int sum(int arr[][COLS]); // 只有声明函数形参的时候才能这样做 列不能省略
int sum(int (*arr)[COLS]);
vector
cpp
vector<V> vertices;
vertices.reserve(3); // 仅仅开辟大小为3的内存空间 如果用vector<V> vertices(3) 会构造3个V对象
vertices.emplace_back(1, 2, 3); // 用这个参数去构造一个V对象
vertices.push_back(V(4, 5, 6)); // main中会进行拷贝构造
for(const V& tmp : vertices) {
cout << tmp << endl;
}
vertices.erase(vertices.begin());
是在堆上创建底层数据存储
C++静态数组
array
存储在栈上,没什么性能成本,调试模式下有
cpp
#include <array>
int main() {
std::array<int, 5> data;
data[0] = 2;
data.size(); // 实际上没有变量存储size,他是函数模板参数,直接返回的5
}
如何把他作为参数传递,不知道数组大小
C++库
静态链接
简单来说,静态链接意味着这个库会放到你的可执行文件中,动态链接库是在运行时被链接的,所以可以选择在程序运行时,装载动态链接库。
主要区别就是库文件是否被编译到exe文件或者链接到exe文件中。
步骤:
- 右键项目,C/C++,常规,附加包含目录,${SolutionDir}Dependenceies...
- 右键项目,链接器,附加依赖项,指定相对于库目录的库文件的名称
- 右键项目,链接器,常规,附加库目录
动态链接
- 右键项目,链接器,附加依赖项,指定相对于库目录的库文件的名称 修改为xxxdll.lib
- 接下来简单做法是把dll复制到可执行文件的地方
VS中创建多个项目和库
- 对于生成可执行文件的项目A,右键属性,配置属性,配置类型(应用按程序)
- 对于要静态链接的项目B,配置类型设置为静态库
- 然后在A中设置,C/C++,常规,附件包含目录中设置为B的头文件所在文件夹
- A的main.cpp中直接引入B头文件,并在B中点击build,会生成lib文件
- 右键A,添加,引用,选择B项目,剩下的都会自动完成
此时A依赖B,B发生变化,如果我们编译A,实际编译B和A
C++ 返回多个返回值
创建一个结构体,在里面设置你要返回的多个变量。
传引用也是可以的。
C++友元
能够在类外访问一个类的私有成员
1.友元(全局)函数
cpp
class Building {
friend void visit(Building& b); // 友元(全局)函数 也可以将实现写到这里
public:
string m_SittingRoom;
Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom;
}
void visit(Building& b) {
cout << b.m_BedRoom << endl; // 访问私有变量
}
2.友元类
cpp
class Building;
class Friends {
public:
void visit(Building & b);
};
class Building {
friend class Friends;
public:
string m_SittingRoom;
Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom;
};
void Friends::visit(Building & b) {
cout << b.m_BedRoom << endl; // 访问私有变量
}
3.友元成员函数
cpp
class Building;
class Friends {
public:
void visit(Building & b);
};
class Building {
friend void Friends::visit(Building & b);
public:
string m_SittingRoom;
Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom;
};
void Friends::visit(Building & b) {
cout << b.m_BedRoom << endl; // 访问私有变量
}
注意事项
1.友元关系不能被继承
2.友元关系是单向的
3.友元关系不具有传递性
C++类模板(泛型程序设计)
泛型程序设计
大量编写模板、使用模板的程序设计
分为函数模板和类模板
函数模板:实际建立一个通用函数 ,它用到的数据的类型(返回值类型、形参类型、局部变量类型)可以具体不指定,用一个虚拟类型替代(标识符占位),等发生函数调用再根据传入的实参来逆推真正的类型(这时函数才被真正的创建) 。数据的值和类型都被参数化了
template<typename T1, typename T2>
cpp
template<typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int a = 20, b = 30;
mySwap(a, b);
}
cpp
template<typename T>
void fun();
---
//fun(); // 模板必须确定T的类型
fun<int>();
普通函数和函数模板调用 :优先考虑普通函数的调用。想调用函数模板,在调用时需要加上空模板参数列表 myPrint<>(a, b); 函数模板也可以重载。如果函数模板能产生更好的匹配,优先采用函数模板
机制:
1.编译器不是把函数模板处理成能处理任何类型的函数
2.函数模板通过具体类型产生不同函数
3.编译器会对模板函数进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
对于特殊的数据类型,可以具体化实现来解决问题 template<> 返回值类型 函数名(参数列表) {}
cpp
template<> bool myCompare(Person& p1, Person& p2) {
}
没调用是不会生成具体函数的
类模板
cpp
template<class NAMETYPE, class AGETYPE = int> // 模板中可使用默认参数
class Person {
public:
Person(NAMETYPE name, AGETYPE age) {
this->m_Name = name;
this->m_Age = age;
}
NAMETYPE m_Name;
AGETYPE m_Age;
}
int main() {
Person<string, int> p1("tom", 20); // 类模板不能自动类型推导,需要显示指定
}
成员函数创建时机:在使用的时候才生成
类模板做函数参数
cpp
template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
T1 m_Name;
T2 m_Age;
void show(){};
}
//1.指定传入的具体类型
void dowork(Person<string, int>& p) {
p.show();
}
//参数模板化
template<class T1, class T2>
void dowork2(Person<T1, T2>& p) {
p.show();
}
// 类模板化
template<class T>
void dowork3(T& p) {
p.show();
}
类模板在继承中的问题
cpp
template <class T>
class Base {
public:
T a;
};
class Son : public Base<int> { //必须明确父类的泛型的参数
};
template <class T1, class T2>
class Son2 : public Base<T2> {};
类模板成员函数类外实现
cpp
template <class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) { //类外,类内要声明
}
分文件编写时,把实现也写在.h文件中
类模板与友元
类内实现:
cpp
#include<bits/stdc++.h>
using namespace std;
template<class T1, class T2>
class Person {
friend void print(Person<T1, T2>& p) { // 全局友元函数
cout << p.name << " " << p.age << endl;
}
public:
Person(T1 name, T2 age) {
this->age = age;
this->name = name;
}
private:
T1 name;
T2 age;
};
int main() {
Person<string, int>p("tom", 22);
print(p);
return 0;
}
类外实现
cpp
#include<bits/stdc++.h>
using namespace std;
//先让编译器看到Person
template<class T1, class T2> class Person;
//先让编译器看到print的存在 没有尖括号
template<class T1, class T2> void print(Person<T1, T2>& p);
template<class T1, class T2>
class Person {
// friend void print(Person<T1, T2>& p); 是函数,参数是类模板
friend void print<>(Person<T1, T2>& p); // 这是一个模板
public:
Person(T1 name, T2 age) {
this->age = age;
this->name = name;
}
private:
T1 name;
T2 age;
};
// 这是一个函数模板
template<class T1, class T2>
void print(Person<T1, T2>& p) {
cout << p.name << endl;
}
int main() {
Person<string, int>p("tom", 22);
print(p);
return 0;
}
C++线程
cpp
#include <iostream>
#include <functional>
#include <thread>
static bool s_Finished = false;
void DoWork() {
using namespace std::literals::chrono_literals;
while (!s_Finished) {
std::cout << "working\n";
std::this_thread::sleep_for(1s);
}
}
int main(int argc, char* argv[]) {
std::thread worker(DoWork);
std::cin.get();
s_Finished = true;
worker.join();
std::cout << "F" << std::endl;
std::cin.get();
return 0;
}
C++计时
cpp
#include <iostream>
#include <string>
#include <chrono>
class Timer {
public:
Timer() {
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}
~Timer() {
Stop();
}
void Stop() {
auto endTimepoint = std::chrono::high_resolution_clock::now();
// time_since_epoch() 是时间起始点到现在的时长,也可以用milliseconds,但是这个例子要精确的
auto start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
auto end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();
auto duration = end - start;
double ms = duration * 0.001; //微秒变毫秒
// 输出
std::cout << duration << "us " << ms << "ms" << std::endl;
}
private:
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint;
std::chrono::duration<float> duration;
};
int main()
{
{
Timer timer;
for (int i = 0; i < 100; i++) {
int* tmp = new int[50];
delete[] tmp;
}
}
}
C++异常
异常
cpp
try {
} catch(ExceptionName e) {
} catch(...) { // 其他异常捕获
}
您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。处理不了的异常,可以在最后一个分支用 throw 向上抛出
cpp
#include <iostream>
#include <exception>
using namespace std;
class MyException : public exception {
public:
const char* what() const throw(){
return "C++ Exception";
}
};
int main() {
try{
throw MyException();
}
catch(MyException& e) {
cout << "MyException caught" << endl;
cout << e.what() << endl;
}
catch(std::exception& e) {
//其他的错误
}
}
栈解旋
异常被抛出后,从进入try块起,到异常被抛前,期间栈上构造的对象,都会自动析构。析构顺序和构造顺序相反,成为栈的解旋。
C++IO
IO流
cpp
void test1() {
ofstream ofs("test.txt", ios::out | ios::trunc);
if(!ofs.is_open()) {
cout << "failed" << endl;
return;
}
ofs << "13222" << endl;
//关闭
ofs.close();
}
void test2() {
ifstream ifs("test.txt", ios::in);
if(!ifs.is_open()) {
cout << "failed" << endl;
return;
}
char buff[1024] = {0};
/* while(ifs >> buff) {
cout << buff << endl;
}*/
/* while(ifs.getline(buff, sizeof(buff))) {
cout << buff << endl;
}*/
string buf;
while(getline(ifs, buf)) {
cout << buf << endl;
}
//关闭
ifs.close();
}
C++函数对象
是一个类不是一个函数,重载了()操作符让它可以像函数一样调用,也叫仿函数,可以拥有一些状态,比如定义一些成员变量
cpp
class Print {
public:
void operator()(string t) {
cout << t << endl;
}
}
int main() {
Print p;
p("123") << endl;
}
STL
C++谓词
谓词(predicate)可以是一个返回值,是bool类型的普通函数或者仿函数。标准库用到的谓词要么是一元谓词(接受一个参数),要么是二元谓词(接受两个参数)。
sort算法中默认使用<运算符进行比较,可以定义一个二元谓词代替<进行比较
cpp
bool greater10(int value) {
return value > 10;
}
bool twice(int elem1, int elem2) {
return (elem1 * 2) == elem2; // 判断两倍关系
}
C++内建函数对象
STL内建了一些函数对象
- 这些仿函数所产生的对象,用法和一般函数完全相同
- 使用内建函数对象,需要引入头文件
<functional>
算术仿函数
- 实现四则运算
- 其中negate是一元运算,其他都是二元运算
cpp
template<class T> T plus<T> //加法仿函数
template<class T> T minus<T> //减法仿函数
template<class T> T multiplies<T> //乘法仿函数
template<class T> T divides<T> //除法仿函数
template<class T> T modulus<T> //取模仿函数
template<class T> T negate<T> //取反仿函数
#include <iostream>
#include <functional>
using namespace std;
//negate 一元仿函数 取反仿函数
void test() {
negate<int> n;
cout << n(50) << endl;;
}
//plus 二元仿函数 加法
void test1() {
plus<int>p;
cout << p(10, 20) << endl;
}
int main() {
test();
test1();
return 0;
}
关系仿函数
cpp
template<class T> bool equal_to<T> //等于
template<class T> bool not_equal_to<T> //不等于
template<class T> bool greater<T> //大于
template<class T> bool greater_equal<T> //大于等于
template<class T> bool less<T> //小于
template<class T> bool less_equal<T> //小于等于
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
using namespace std;
class myCompare {
public:
bool operator()(int val1,int val2) {
return val1 > val2;
}
};
void test02() {
vector<int>v;
v.push_back(1);
v.push_back(3);
v.push_back(4);
v.push_back(2);
v.push_back(5);
//遍历
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout<<(*it)<<"\t";
}
cout<<endl;
//通过算法降序 //利用仿函数对象
//sort(v.begin(), v.end(), myCompare());
sort(v.begin(), v.end(), greater<int>()); //greater<int>()内建函数对象
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << (*it) << "\t";
}
}
int main() {
test02();
return 0;
}
逻辑仿函数
cpp
template<class T> bool logical_and<T> //逻辑与
template<class T> bool logical_or<T> //逻辑或
template<class T> bool logical_not<T> //逻辑非
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
using namespace std;
void test03() {
vector<bool>v;
v.push_back(false);
v.push_back(true);
v.push_back(false);
v.push_back(false);
v.push_back(true);
//遍历
for (vector<bool>::iterator it = v.begin(); it != v.end(); it++) {
cout << (*it) << "\t";
}
cout << endl;
//利用逻辑非 将容器v的数据搬运到v2中,并执行取反操作
vector<bool> v2;
//先开辟空间,再搬运
v2.resize(v.size());
//有四个参数,分别是原数据的起始位置、原数据的终止位置、目的位置的起始位置、操作运算
transform(v.begin(), v.end(), v2.begin(), logical_not<bool>());
for (vector<bool>::iterator it = v2.begin(); it != v2.end(); it++) {
cout << (*it) << "\t";
}
cout << endl;
}
int main() {
test03();
return 0;
}
C++适配器
适配器, 在STL中扮演着转换器 的角色,本质上是一种设计模式,用于将一种接口转换成另一种接口 ,从而是原本不兼容的接口能够很好地一起运作。
根据目标接口的类型,适配器可分为以下几类:
- 改变容器的接口,称为容器适配器;
- 改变迭代器的接口,称为迭代器适配器;
- 改变仿函数的接口,称为仿函数适配器。
容器适配器
容器的适配器有stack、queue、priority_queue,是在容器deque的基础进行了一些特定的约束,因而本质上并不属于容器,而是容器的适配器。
比如:
queue是一个队列,实现先进先出 功能,queue不是标准的STL容器,却以标准的STL容器为基础 。queue是在deque的基础上封装的。之所以选择deque而不选择vector是因为deque在删除元素的时候释放空间,同时在重新申请空间的时候无需拷贝所有元素。
stack是实现先进后出的功能,和queue一样,也是内部封装了deque。stack的源代码原理和实现方式均跟queue相同。
函数适配器
有时候需要对函数返回值进行进一步的简单计算,或者填上多余的参数,不能直接代入算法,函数适配器实现了这一功能,将一种函数对象转化为另一种符合要求的函数对象
texxt
bind1st(op,value) 辅助构造binder1st适配器实例,绑定固定值到二元函数的第一个参数位置
bind2nd(op,value) 辅助构造binder2nd适配器实例,绑定固定值到二元函数的第二个参数位置
not1(op) 辅助构造unary_negate适配器实例,生成一元函数的逻辑反函数
not2(op) 辅助构造binary_negate适配器实例,生成二元函数的逻辑反函数
mem_fun_ref(op) 辅助构造mem_fun_ref_t等成员函数适配器实例,返回一元或二元函数对象
mem_fun(op) 辅助构造mem_fun_t等成员函数适配器实例,返回一元或二元函数对象
ptr_fun(op) 辅助构造一般函数指针的pointer_to_ unary_function或pointer_to_ binary_function适配器实例
...
通过bind,我们将仿函数与参数进行绑定,可实现算法所需的条件判断功能,例如判断小于12的元素时,可使用bind2nd(less<int>(),12)
,就可以达到目的。
否定(negate)这里就是取反的操作,例如not1(bind2nd(less<int>(),12))
,就可判断不小于12的元素。
成员函数适配器for_each(v.begin(), v.end(),mem_fun_ref (&Person::showPerson));
迭代器适配器
C++简单跟踪内存分配方法
cpp
#include <iostream>
#include <string>
#include<memory>
struct AllocationMetrics {
uint32_t TotalAllocated = 0;
uint32_t TotalFreed = 0;
uint32_t CurrentUsage() {
return TotalAllocated - TotalFreed;
}
};
static AllocationMetrics s_Allocationmetrics;
void* operator new(size_t size) {
//std::cout << "Allocating" << size << "bytes\n";
s_Allocationmetrics.TotalAllocated += size;
return malloc(size);
}
void operator delete(void* memory, size_t size) {
s_Allocationmetrics.TotalFreed += size;
//std::cout << "Freeing" << size << "bytes\n";
free(memory);
}
struct Object {
int x, y, z;
};
static void PrintMemoryUsage() {
std::cout << s_Allocationmetrics.CurrentUsage() << std::endl;
}
int main() {
PrintMemoryUsage();
std::string s = "yy";
PrintMemoryUsage();
{
std::unique_ptr<Object> obj = std::make_unique<Object>();
PrintMemoryUsage();
}
PrintMemoryUsage();
}
C++的小字符串优化
对于不超过15个字符(这个数字取决于编译器版本),它不会再堆上分配,会再栈缓冲区分配。
所以对于很小的字符串,就不需要使用const char*,或者试图优化你的代码,因为这很可能不会导致堆分配。(Debug模式下会不一样)
C++17std::string_view
本质只是一个指向现有内存的指针const char*,指向别人拥有的现有的字符串,再加一个大小size。所以不需要分配一个新的字符串,或者用substr()创建一个新的。观察如下代码
cpp
#include <iostream>
static uint32_t s_AllocCount = 0;
void* operator new(size_t size) {
s_AllocCount++;
return malloc(size);
}
#define STRING_VIEW 1
#if STRING_VIEW
void Print(std::string_view name) {
std::cout << name << std::endl;
}
#else
void Print(const std::string& name) {
std::cout << name << std::endl;
}
#endif
int main() {
std::string name = "yang yuyyyyyyyyyyyyyyyyyyy";
#if STRING_VIEW
std::string_view lastName(name.c_str(), 4); //c_str() 是字符串的const char* 类型
std::string_view firstName(name.c_str() + 5, 2);
#else
std::string firstName = name.substr(5, 7);
std::string lastName = name.substr(0, 4);
#endif
Print(firstName);
std::cout << s_AllocCount << std::endl;
}
我们甚至可以const char* name = "yang yuyyyyyyyyyyyyyyyyyyy"; 再把name.c_str() 改成name,就没有堆分配了
C++17
结构化绑定
用于处理多返回值
cpp
#include <tuple>
std::tuple<std::string, int> CreatePerson() {
return {"yy", 22};
}
int main() {
auto[name, age] = CreatePerson();
std::cout << name;
}
完全没必要因为返回值,就编写只用一次的结构体。
std::optional
简洁而直观的方式来表示一个值可能存在或不存在的情况
cpp
#include<iostream>
#include<optional>
#include<fstream>
std::optional<std::string> ReadFileAsString(const string& filePath) {
std::ifstream stream(filePath);
if (stream) {
std::string result;
// read file
stream.close();
return result;
}
return {};
}
int main() {
std::optional<std::string> data = ReadFileAsString("data.text");
std::string value = data.value_or("Not Present"); // 不存在默认为Not Present
std::optinal<int> count;
int c = count.value_or(100); //不存在默认为100
if (data) {
std::cout << "File read successfully\n";
}
else {
std::cout << "failed\n";
}
}
std::viriant
单一变量存储多个类型。列出它可能的类型,然后决定它是什么,
cpp
#include <iostream>
#include <variant>
#include <string>
int main() {
std::variant<std::string, int> data;
data = "yy";
std::cout << std::get<std::string>(data) << "\n";
//如果data类型是string
if(auto value = std::get_if(std::string)(&data)) {
std::stirng& v = *value
} else {}
}
-------
enum ErrorCode {
None = 0, NotFound = 1, NoAccess = 2
};
std::variant<std::string, ErrorCode> ReadFileAsString() {
return {}; // 可以返回更多的数值
}
std::any
存储任意类型数据
cpp
#include <ant>
int main() {
std::any data;
data = 2;
data = std::string("yy");
std::string str::ant_cast<std::string>(data);// 如果data不是想要转换的类型,会抛出类型转换异常.
或者可以
std::string& str::ant_cast<std::string&>(data); //主义模板参数
}
和variant区别:
- vatirant只是一个类型安全的union,把所有数据存在了union里面
- std::any:对于small type 只是存储为union,这和vatirant工作方式相似,大类型会进入大存储空间的void*,它会动态分配内存
所以variant处理较大数据的时候执行的更快,因为避免了动态内存分配
一般都是用variant