C++学习笔记(基础篇)
目录
移动运算符std::move()与移动构造函数与移动赋值操作符
写在前面
当我写这个时,总感觉有总囫囵吞枣的感受,C++内容确实很多,学习和实践的记录也显得片面。
正文
C++和C的区别:
1:使用了命名空间
2:作用域标识符 ::
3:结构体可当类使用,定义方法等。
4:引用类型 &,在C中是取地址
5:函数重载(操作符重载)
6:面向对象
等等等等
注意点:
结构体和类的区别在于,结构体中的变量和方法都是public,而类默认是private
const的用法:
<1
const在C或者是C++中被指的是不可改变的,在类中可以看做是一种可见性 ,它更像是一种承诺,承诺该值不被改变,但也是可以绕过其承诺比如通过逆向引用其地址可以修改const修饰的值
std::cout << "Hello World!\n";
const int a = 12;
int* b = NULL;
b = (int*) & a;
*b = 14;
std::cout << *b << std::endl;
std::cout << a << std::endl;
为了更加严谨的保证承诺应修改为const int * b , const放*前面,保证b这个指针可以修改其地址指向别人,但他指向的内容不能被修改,被网友称为常量指针,它还有另一种写法 int const * b(只要保持在const在*前则为常量指针)。于此相反,const放在*后面int * const b则内容可以更改,但指针的值也就是b也就是所指向的地址不能被修改,这就是引用&的本质。在网上被称为指针常量。而const int * const b,则指向的内容和指向的地址都不能被改变。
<2
在类中被const标记的方法则不允许类成员的值发生改变 如int getter() const;
在该类方法中则不允许类成员的值发生改变。而以mutable(意味着这个值是可以被改变的)修饰的变量则可以在const标记的方法中改变其值。
<3 带const标记和不带const的同名方法可被重载(返回值类型和参数类型都相同),默认情况下则会调用const标记的方法。
构造函数的初始化列表
初始化值的顺序与类成员变量声明的顺序保持一致,因为创建对象时会先调用成员初始化列表,不在列表中的值也会被初始化,所以用了初始化列表后为了避免性能上的浪费(多次初始化),在初始化列表中初始化所有成员变量。比如
class example {
public:
int s;
example()
{
std::cout << "i am inited";
}
example(int x) {
std::cout << x << std::endl;
}
};
class test {
public :
int a;
example c;
test()
:a(1)
{
c = example(7);
}
};
定义类test和初始化列表时则会调用example的默认初始化构造函数,在构造函数里又重新定义则在栈上创建了两个example对象。已经用const修饰的成员变量只能用初始化列表初始化且必须被初始化。
而当类成员是一个类的实例,且该实例中有变量用const修饰,则该类成员必须用初始化列表实现,而不能在其构造函数中,编译器也会报错,因为const修饰的值只能在初始化列表中被初始化且不能修改,用了初始化列表则按类成员声明都被初始化一次,在构造函数中再初始化,该类成员又会调用其构造函数又初始化一遍const,显然这是不被允许的。
class example {
public:
const int s;
example()
:s(78)
{
std::cout << "i am inited"<<std::endl;
}
example(int x)
:s(1)
{
std::cout << x << std::endl;
}
};
class test {
public :
int a;
example c;
test()
:a(1)
{
c = example(6);
}};
比如标红的这句代码不被允许
<4 const指针只能调用const方法
new和malloc的区别
都是分配内存的大小,而new仅仅只是调用了其构造函数(如创建类对象)都是在堆上创建空间,注意释放内存),在一个特定的地址初始化对象。
Entity*p=new(指向内存的指针)Entity();
例如
int* b = new int(50);
Entity* p = new(b)Entity();
std::cout << b << std::endl;
std::cout << p << std::endl;
隐式构造函数
当一个类中有多个重载的构造方法接受一个参数时,若是直接以值直接赋值给对象的形式则为隐式构造函数。在函数传参进行隐式构造对象时,字符串赋值可以用std::string(“abcc”)或直接
Entity(“avc”)
如
#include <iostream>
using namespace std;
class Entity {
public:
int num;
std::string name;
Entity() :num(1), name("sda")
{}
Entity(const std::string& name) :num(-1),name(name)
{}
};
void Printmessage(const Entity &t) {
std::cout << t.name << t.num << std::endl;
}
int main()
{
Entity t1 = "sda";
Printmessage(t1);
Printmessage(std::string("dada"));
return 0;
}
这种写法通常用于简化代码,实际用到少。
而用explicit修饰构造函数则不允许调用隐式类型转换,也就是只允许显式调用其构造函数。如
explicit Entity(const std::string & name :num(1), name(name)
{}
除非使用强制类型转换
Entity e=(Entity)"sdad";
操作符的重载
本质上就是函数。
格式:
返回值类型 operator 操作符号 (参数){
函数体
}
比如
class Entity {
public:
int num;
std::string name;
Entity() :num(1), name("sda")
{
}
explicit Entity(const std::string& name) :num(-1),name(name)
{
}
};
std::ostream& operator<<(std::ostream& stream, const Entity& e) {
stream << e.name << "," << e.num;
return stream;
}
int main()
{
Entity t1 = (Entity)"sda";
std::cout << t1 << std::endl;
return 0;
}
类中的this关键字
在类中this关键字是指当前类对象的指针,且该指针的值也就是其指向的地址不允许发生修改,它是一个指针常量。
作用域
像空作用域 {} 其他的如 if for while 函数体内,类内都是作用域。在作用域内使用堆开辟内存则需要手动释放,即使是用了智能指针也是帮我们在离开作用域时做了手动释放内存。而一般的定义变量则是在栈开辟的内存,离开作用域后自动释放。
{
Entity* t1 = new Entity();
}
t1在离开{}作用域后被释放,但其指向的空间由于是堆开辟的则未被释放。
智能指针
std::unique_ptr 作用域指针
{
std::unique_ptr<Entity> e1 = std::make_unique<Entity>();
std::unique_ptr<Entity> e2(new Entity());}
为什么叫unique_ptr?它表示你不能复制它给第二个unique_ptr,它的拷贝构造函数是已经被删除的了。如果存在两个unique_ptr指向同一个内存地址,当一个unique_ptr超出作用域被释放时,它的一个复制unique_ptr将指向无效的区域,显然这是不被允许的。
unique_ptr在离开作用域后自动释放堆分配的空间。
unique_ptr的构造函数是用explicit修饰的,则意味着只能通过显式构造创建。它是一个栈分配的对象,但栈对象死亡时,它会调用delete在其指针上以释放内存空间,它是低开销甚至没有开销的。
std::make_unique是c++ 14的引用的,C++11并不支持,好处在于可以抛出创建对象时构造函数异常,而不会得到一个没有引用的悬空指针造成内存泄漏,可以认为是异常安全的。
std::share_ptr 共享指针
它则允许被复制,它被创造时需要分配另一块内存用于引用计数,复制该共享指针一次则引用计数+1,释放一次引用计数-1,只要其引用计数不为0,共享指针所指向的内存区域都不会被释放。
目前我认为本质是是一种以控制栈对象的释放来控制堆区域内存的释放。
std::weak_ptr 弱指针
但把一个共享指针复制给一个弱指针时,引用次数不会+1,但其共享指针被释放后,weak_ptr则指向无效区域。
智能指针为自动化内存管理提供了便利。
拷贝构造函数(默认浅拷贝需要自己写深拷贝)
如果一个类中有指针这类数据类型,用一个已有的该类对象去初始化,如果不自己实现深层拷贝构造函数,那么类本身是有一个浅层的拷贝构造函数(复制原对象给新对象),所以指针这类数据的值也被复制了,所以调用默认的浅层拷贝构造创建的对象中的指针指向的是同一个内存空间,而其他的数据类型(包括指针本身所占内存)都被重新分配了空间。比如用A去初始化B,问题在于A出了作用域调用析构函数时,A的指针所指向的内存空间被释放,该被释放的空间已经被标记为不属于该对象,B调用析构函数二次释放出现问题。所以自己需要定义一个深层拷贝构造函数:再次为指针开辟新的空间,并拷贝其值。
比如
#include <iostream>
using namespace std;
class Entity {
public:
int a;
int* b;
Entity(int v_a) :
a(v_a)
{
std::cout << "haved been created" << std::endl;
b = new int;
*b = 12;
}
Entity(const Entity& t)
:a(t.a)
{
this->b = new int;
*(this->b) = *(t.b);
}
~Entity() {
std::cout << "haved been destroyed" << std::endl;
delete this->b;
}
};
int main()
{
{
Entity t1(2);
Entity t2(t1);
*(t1.b) = 99;
std::cout << *(t1.b) << std::endl;
std::cout << *(t2.b) << std::endl;
std::cout << t1.b << "," << t2.b << std::endl;
}
return 0;
}
在函数传递对象且不改变对象内容时,总是使用const +对象的引用形式去传递(考虑到深层拷贝消耗内存导致程序变慢)
动态数组
std::vector<元素类型> 动态数组名
获取大小 .size()
.push_back()新增元素
.clear() 清除所有元素
.erase()清除某个元素 参数为迭代器 .begin()+1 (移除第2个元素从0开始)
.reserve(x)
当动态数组做函数参数时,使用引用类型(避免在栈内开辟大量空间)
对于动态数组效率的优化:
<1>与编译器的环境有关,一般创建动态数组时,默认数组大小为1,这意味着每次添加新的元素都会将数组进行扩容然后再拷贝,效率慢。一个有效的方法是通过.reserve(x)告诉动态数组里面初始会存多少个元素,虽然这只是一个大概值,能减少从1到x容量的拷贝。(其中x是动态数组中元素容量的大小)
<2>使用emplace_back代替push_back(),在这种情况下不是传递已新建对象,而是传递构造函数的参数列表,并让动态数组使用该参数列表新建对象。
静态链接和动态链接库
区别:
静态链接库会放入可执行文件中参与整个编译过程,而动态链接库是运行时链接的,你可以选择在程序运行时,装载动态链接库,以节省内存资源。静态链接可以产生更快的应用程序。而当动态链接库被运行时的程序装载时,程序的部分将被补充完整。
DLL是一种运行时的动态链接库,lib文件也被称为是静态连接库。
对于静态链接库:
xxx.lib为静态库,设置环境,如库里面的函数声明头文件的目录,包含库目录以及附加依赖项(库名称)
对于动态链接库:
xxxdll.lib(包含指向dll文件的指针)与 xxx.dll为动态库,里面包含一堆函数指针指向dll 可执行文件
总的来说就是两个东西:一个头文件(包含声明) 一个库(包含实现)
静态库实例
值得注意的是下载的库与运行平台对应 32位对应32位运行平台
以GLFW这个图形库为例(在C下编译的库)
(以相对于解决方案的路径)
首先添加包含目录(头文件所在目录)
包含库目录
添加附加依赖性
另外也可以不添加头文件(由于GLFW是C下建立的库,故extern),再提供声明就能链接到一个C语言下的库,也就是在附加依赖项中的具体实现。这就是第一步添加头文件包含目录的作用。
#include "ostream"
//#include "GLFW/glfw3.h"extern "C" int glfwInit();
#include <iostream>
int main() {int a = glfwInit();
std::cout << a;
}
动态库实例
可执行文件知道动态库的存在,在运行时加载,正如我有时装环境报错缺少 dll 文件一样。。。
动态库其中的之一是 "静态的动态库"
即 应用程序知道动态库里有什么函数,它能使用使用什么
动态库的另一个版本是任意加载动态库,不需要知道库里面有什么
以第一种来看的动态库配置(以glfw同时支持静态库和动态库为例)
与静态库配置不同的是附加依赖项改为glfwdll.lib。这时程序已经能编译成功生成可执行文件了。
运行!
正如上述,glfwdll.lib包含的是dll的文件指针,但却找不到dll。
解决方法需要在一个可以访问的地方添加dll,可以在应用程序中设置搜索位置。
而可执行文件exe的根目录下是一种自动搜索路径,不妨把dll(如glfw3.dll)防止可执行文件.exe的根目录下
运行结果正常。
函数的多返回值
在知道返回值类型的情况下,使用array ,vector数组返回(返回变量的类型相同)
几种灵活的方式:(返回不同的变量类型)
<1>使用自定义的结构体
typedef struct myres {
std::string s1;
std::int16_t a;
} myres;
myres RetTest() {
return { "dada",111 };
}
int main() {
myres t = RetTest();
std::cout << t.a << t.s1<< std:: endl;
}
<2>使用pair
std::pair < std::string, std::int16_t> t;
t.first = "sada";
t.second = 2;
C++的模板
模板只有在被调用时才会被实际创建,基于模板的使用情况进行编译,不调用则不参与编译。因为它只是一个模板,并不是实际代码。
它可以做泛型使用,但不仅仅是泛型。
比如充当泛型
template <typename T>
void Print(T val) {
std::cout << val << std::endl;
}
template<int size>
class myvector
{
private:
int Array[size];
public:
myvector() {
}
~myvector() {
}
int getsize() {
return size;
}
};
int main() {
Print(2);
Print("dakdha");
myvector<15613> t;
std::cout << t.getsize() << std::endl;
}
堆内存与栈内存的比较
栈内存:
在C++中吗,栈内存和堆内存都是指内存RAM分配的空间,一般栈内存大小只有2M,且经常被频繁访问,比如函数内部定义的局部变量都是在栈内开辟,栈内存非常高效且快捷,从底层汇编代码看就是一条指令,注意,栈上的对象在离开作用域后都会被出栈,原内存被标明为可用内存从而等待下一次分配。
堆内存:
有时需要定义大的空间的数据则在堆上开辟内存,它会设计更加复杂的代码,比如反复的移动内存数据等,需要手动释放内存。
C++的宏
和C一样,以#开头,其代码被编译器评估替换后参与编译。
比如
#define MAIN int main()\
{\
cout<<"hello"; \
}
MAIN;
auto关键字
给我感觉像是弱定义语言,比如JS里的var,由编译器自动标明其数据类型。
C++11中函数的返回可以是auto 类型。这通常是两面性的,比如在api调用的时候,好处是在上层api更改其数据类型时,下层用var去接受则不需要去修改代码,而下层的数据类型被自动标明则在后续的代码中存在不可预知的隐患。
通常使用场景:
数据名过长,可以取别名或者用auto去定义。
一般的如int float 等
using myint = int;
myint a = 2;
auto b = a;
常见数据类型不建议用auto,显得代码可读性差。
静态数组
一个被限制大小的数组,其数组大小也就是长度是可知的。
以下例子以模板的形式打印任何数组的长度
template <int _size,typename T>
void test3(const std::array <T ,_size> &t)
{
for (int i = 0; i < _size; i++) {
std::cout << t[i] << std::endl;
}
}
int main() {
std::array<int, 5> s;
s[1] = 1;
s[2] = 3;
s[0] = 0;
test3(s);
}
函数指针
返回类型 (* 名字)(参数列表)
比如
void ( *hello)();
它表示定义了一个指针,指针名为hello,指向一个函数,函数的返回类型是void ,函数的参数列表为最后一个()。
见以下三种方式
void hello(int a) {
std::cout << "value:" << a << std::endl;
}
int main() {
//定义一个函数指针名为test1 指向hello
void(*test1)(int) = hello;
test1(2);
//auto 自动赋值其数据类型 其数据类型为 void(*)(int a)
auto test2 = hello;
test2(2);
test2(3);
//取别名
typedef void(*test3)(int);
test3 b = hello;
b(88);
}
实用的例子
#include "ostream"
#include "GLFW/glfw3.h"
#include "array"
#include <iostream>
#include "head.h"
#include <vector>
using namespace std;
void PrintValue(int a) {
std::cout << "value:" << a << std::endl;
}
void ForEach(const std::vector<int>& arr, void(*print)(int)) {
for (int val : arr) {
print(val);
}
}
int main() {
std::vector<int> arr = { 1,2,3,9 };
//通过函数指针传入
ForEach(arr, PrintValue);
//通过lambda表达式 匿名函数传递函数指针对象
ForEach(arr, []( int val) {std::cout << val << std::endl; });
}
C++的lambda
它本质上是我吗定义匿名函数的一种方式,lambda是我们不需要通过函数定义就能够定义一个函数的方法(说的有些啰嗦,可以看上面的例子,只要有函数指针就能够通过lambda定义匿名函数)
所以它的作用在于传递一个函数,这个函数将会被调用执行,就是充当回调函数。
常用的格式
[campture(捕获方式](参数列表){函数体}
int main() {
auto fun = [](int a) {
std::cout << "val:" << a << std::endl;
return 1;
};
std::cout << fun(2112) << std::endl;
}
其中捕获方式指传值的方式。
注意非捕获lambda可以隐式转换为函数指针,而非捕获lambda则不可以转换为函数指针
有:
- [ = ] 值传递,拷贝传入的值, 原传入值不允许修改 ,除非使用mutable修饰,则允许修改拷贝后的值,(但原值依旧是不变),这一点非常的古怪,所有值传递
- [ & ] 值引用 允许修改 所有值都引用
- [ 具体值 ] 只对单个值进行操作
mutable修饰
auto fun = [=] (int a) mutable {
std::cout << "val:" << a << std::endl;
std::cout << b++ << std::endl;
};
fun(10);
std::cout << b << std::endl;
传入值b ,在lambda表达式中的b是拷贝后来的值,而非原值,古怪就在于表达式中使用的名字是相同的。
运行结果:
见例子
int main() {
//值传递
int b = 3;
int a = 4;
auto fun = [=](int a) {
std::cout << "val:" << a << std::endl;
std::cout << b << std::endl;
};
fun(10);
std::cout << b << std::endl;
std::cout << "--------------------" << std::endl;
//值引入
auto fun2 = [&](int a) {
std::cout << "val:" << a << std::endl;
b++;
};
fun2(10);
std::cout << b << std::endl;
//单个值引入
std::cout << "--------------------" << std::endl;
auto fun3 = [&b,&a](int d) {
std::cout << "val:" << d << std::endl;
b++;
a++;
};
fun3(10);
std::cout << b <<" ,"<< a << std::endl;
}
比如充当一个回调函数,
以find_if 为这个标准库的函数为例,对以下函数的解释:它将被迭代的元素传入我们自己定义的回调函数,然后做判断再返回这个迭代器,再解引用得到返回值。
int main() {
std::vector<int> arr = { 1,2,3,48 };
auto ret= std::find_if(arr.begin(), arr.end(), [](int value) {return value > 10; });
std::cout << *ret << std::endl;
}
C++的namespace
为什么不要在头文件中使用using namespace
- 首先是代码的可读性变差,不知道源代码是属于哪一个命名空间的
- 多个不同的命名空间的相同名字的函数或类在使用时由于精确匹配从而预想使用的不一致,比如函数参数中的char * 可以直接匹配char * 而转换为string 则是隐式转换。
所以 namespace使用在限制其作用域的情况下,比如在一个函数当中或者是在一个cpp文件内使用,而在头文件中使用则实属给自己挖坑。
另外在cpp项目中使用c的代码,且C中的函数与CPP命名空间的函数相同则会立即产生冲突。
namespace test1{
namespace test2 {
void printf() {
std::cout << "hello" << std::endl;
}
}
}
int main() {
test1::test2::printf();
using namespace test1::test2;
printf();
namespace test3 = test1::test2;
test3::printf();
}
C++中的线程
main函数本身是一个线程,同时可以在main函数里开启子线程。
直接通过 std:: thread 创建子线程对象,参数为函数指针,代表这个线程将会执行这个函数。
std::thread worker(work);
join() 这个方法表示主线程等待子线程执行完毕,否则不会执行后面的代码
worker.join();
例子:实现在子线程中等待主线程按回车并打印子线程和主线程的id号
static bool m = false;
void work() {
using namespace std::literals::chrono_literals;
std::cout << std::this_thread::get_id() << std::endl;
while (!m) {
std::cout << "working" << std::endl;
std::this_thread::sleep_for(1s);
}
}
int main() {
//这个函数实际会执行在另一个执行线程中所做的事情,参数传一个函数指针
std::thread worker(work);
std::cin.get();//主线程等待输入
m = true;
worker.join();//它会等待工作线程执行完后再执行后面的代码
std::cout << std::this_thread::get_id() << std::endl;
std::cin.get();
}
C++中的计时——计算代码所跑时间
一般可以通过操作系统提供的计时函数进行计时,计时的分辨率也与平台相关,缺点在于不能跨平台使用。本次学习的是以C++中库的方法,它几乎适用于所有的平台来计算代码运行的时间。
使用chrono库
获取当前时间
std::chrono::high_resolution_clock().now();
计算在明确有1s延时的的代码运行时间:
#include "iostream"
#include "chrono"
#include "thread"
int main() {
using namespace std::literals::chrono_literals;
auto start = std::chrono::high_resolution_clock().now();
std::this_thread::sleep_for(1s);
auto end = std::chrono::high_resolution_clock().now();
std::chrono::duration<float> duration = end - start;
std::cout << duration.count() << "s" << std::endl;
}
运行结果:由于代码本身运行需要时间,所以打印出的时间略大于1s。
通过对象的生存周期来计算运行时间
它原理就是上文的基础,在构造函数中计算start,在折构函数中计算end。
通过{}限制作用域从而执行折构函数
#include "iostream"
#include "chrono"
#include "thread"
struct Timer
{
std::chrono::time_point<std::chrono::steady_clock>start, end;
Timer() {
start = std::chrono::high_resolution_clock().now();
}
~Timer() {
end = std::chrono::high_resolution_clock().now();
std::chrono::duration<float> duration = end - start;
std::cout << duration.count() * 1000.0f << "ms\n";
}
};
int main() {
using namespace std::literals::chrono_literals;
{
Timer t;
std::this_thread::sleep_for(1s);
}
}
///在嵌入式交叉编译环境中
#include <iostream>
#include <chrono>
#include <thread>
struct Timer {
std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
Timer() {
start = std::chrono::high_resolution_clock::now();
}
~Timer() {
end = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> duration = end - start;
std::cout << duration.count() * 1000.0f << "ms\n";
}
};
int main() {
using namespace std::literals::chrono_literals;
{
Timer t;
std::this_thread::sleep_for(1s);
}
}
使用C++内置库进行排序
其时间复杂度是nlogN
std::sort()
该内置库函数在头文件中,故#include "algorithm"
若不提供一个函数指针,对于整数,它会按照升序排序
可以提供一个内置函数如内置的标准库函数或你自定义的匿名函数lambda表达式
提供内置函数:
std::sort(v.begin(), v.end(),std::greater<int>());//从大到小排序
lambda表达式
int main() {
{
std::vector<int> v = { 1,2,13,2 };
//std::sort(v.begin(), v.end(),std::greater<int>());
std::sort(v.begin(), v.end(), [](int a, int b) {
return a < b;//从小到大进行排序
//return a > b; //从大到小
});
for (int i = 0; i < v.size(); i++) {
std::cout << v[i] << "\n";
}
}
}
关于这个例子的内置函数,a代表first ,b代表last如果返回true则a在b前(a>b从大到小),如果返回false ,则b在a前(a<b 从小到大)。
通过自己穿插的内置函数,可以实现剩余序列按一个顺序排序,再把一个特殊的数放在末尾或是第一个
如以下例子,把 13 这个数放第一个位置,剩余位置升序排列
int main() {
{
std::vector<int> v = { 1,2,13,2 };
//std::sort(v.begin(), v.end(),std::greater<int>());
std::sort(v.begin(), v.end(), [](int a, int b) {
if (a == 13)return true;
if (b == 13) return false;
return a < b;//从小到大进行排序
});
for (int i = 0; i < v.size(); i++) {
std::cout << v[i] << "\n";
}
}
C++的类型双关
它指C++做为强类型语言,其数据的数据类型是确定的,当操作其数据的类型时可以当做不同的数据类型去对待。这使得C++操作内存非常的自由。
比如通过解引用操作内存时产生未定义问题
int main() {
int a = 12;
double b = *(double*)&a;
std::cout << b << std::endl;
}
自由操作结构体内存空间。
struct t {
int x;
int y;
};
int main() {
t m = { 1,2 };
int* p = (int*)&m;
std::cout << p[0] << "-" << p[1] << std::endl;
}
C++的联合体
共享相同的内存空间
给出例子:
union t {
int a;
int b;
};int main() {
t q;
q.a = 1.0f;
std::cout << q.b;
return 0;
}
C++的虚折构函数
如果一个基类(父类)被其派生类(子类)继承,那么父类的折构函数一定要被声明为virtual,确保其折构函数是虚折构函数,否则当用父类去指向子类对象时(多态使用时),在销毁这种多态使用的对象时,由于是父类指向子类,它不知道还有另一个折构函数,因为父类的折构函数没有被标记为虚折构函数从而发生内存泄漏。 标记为virtual,意味着C++知道有一个方法,在某种情况下可能被重写的方法,而折构函数标记为虚折构函数不是被重写,而是会加一个折构函数,它会先调用子类的折构函数,然后在层次结构中向上,调用父类折构函数。
例如
class base
{
public:
base() {
std::cout << "creae base \n";
}
virtual~base() {
std::cout << "delete base \n";
}
};
class pai : public base
{
public:
pai() {
std::cout << "creae pai \n";
p = new char[20];
}
~pai() {
std::cout << "del pai \n";
delete[] p;
}
private:
char* p;
};
int main() {
{
base b;
}
std::cout << "----------\n";
{
pai a;
}
std::cout << "----------\n";
{
base* c = new pai();
delete c;
}
}
C++风格的类型转换
static_case 与C语音类型转换是一样的。
dynamic_cast在转换时会做类型上的检测,转换失败则返回null,显得更加安全。
const_cast添加和移除const修饰符
reinterpret_cast 把内存解释成别的类型
一般情况下,用C语言风格的类型转换足够了。。
条件与操作断点
当程序在运行的过程中,有时为了去找存在的bug则在一条语句前新增断点,然后不断的F10去看程序运行的状态,如果想要在调试的过程中添加一些别的代码,我们可能会选择结束调试然后再次编译,然后再重复上面的过程。
所谓调试断点的条件:我们可以输入一些表达式,当其为真时才激活断点(这通常是我们想要的特殊状态),这样只需要一次F10则可跳到真正激活断点的语句上。
所以调试断点的操作: 我们可以在断点处添加操作,加上一些输出信息,而无需重新编译。当工程量非常大时免去编译工程的开发时间。
预编译头文件
一般在大型项目中我们的cpp文件总会去包含一些相同的头文件,比如C++的标准库等等,这意味着每次修改后编译都会重新编译这些头文件,有时我们增加的这些头文件代码甚至超过我们自己写的cpp代码,而使用预编译头文件则可以把那些不会轻易改变的,稳定的头文件(如果是经常变化的,则不应该放在预编译头文件中)只编译一次,它以二进制格式存储,这对编译器来说比单纯的文本处理要快得多。这在项目开发中是必须要使用的。
缺点: 一个文件里包含了预编译的头文件,而预编译头文件往往是有大堆的头文件,这会使得其他开发者并不知道CPP文件所真正需要包含的那些头文件,使可读性变差。
实验
在VS中需要用一个cpp文件来包含预编译的头文件。
比如起名预编译的头文件为 pch.h ,让pch.cpp包含pch.h。
在pch.cpp的属性页: (创建预编译的头文件)
在项目的属性页使用预编译的头文件。
pch.h
#include "iostream"
#include "string"
#include "memory"
#include "thread"
#include "functional"
#include "algorithm"
#include "vector"
//数据结构
#include "stack"
#include "array"
#include "set"
#include "map"
#include "unordered_set"
#include "unordered_map"
#include "windows.h"
未使用预编译头文件编译 3s (第一次编译)
与使用预编译头文件对比 1.7s (第一次编译快一倍)
两者在修改文件内容再次编译 未使用:1.3s 使用:0.4s 。
dynamic cast 动态类型转换
该转换可以认为是CPP转换系统中的一个函数,在运行时会增加额外的开销,它会做类型检查,若类与类并不是同一个类则转换结果返回NULL。该函数只用于多态类型的类型转换,且只适用于C++,而不是C。
为什么它能做检测,因为默认程序会开启运行类型信息记录,动态转换则比较把要比较的两个类的类型信息得出是否是同一个类。
在C++中,会适用多态,让父类去指向子类对象,从而调用子类的方法,当该父类指针所指向的类型是其子类,则时dynamic cast转换为子类无疑是可以的;而把指向子类的指针转换成父类指针无疑是失败的,因为它们本身就属于不同的类型。
class Entity {
public:
virtual void play() {
std::cout << "entiry\r\n";
}
};
class Player : public Entity {
public:
void play() {
std::cout << "player\r\n";
}
};
class Enemy : public Entity {
public:
void play() {
std::cout << "enemy\r\n";
}
};
int main() {
Player* p = new Player();
Entity* entity = new Entity();
Player* en = dynamic_cast<Player*>(entity);
if (en) {
std::cout << "success\r\n";
}
else {
std::cout << "fail\r\n";
}
//调用父类方法
entity->play();
// 隐式转换
entity = p;
// 调用子类方法
entity->play();
en = dynamic_cast<Player*>(entity);
if (en) {
std::cout << "success\r\n";
}
return 0;
}
C++17新特性——结构化绑定
正如前面所提到的函数多返回值处理,可以用turple,pair,或者是自定义的结构体进行函数返回数据的处理。 而C++17的新特性则能用结构化绑定的方式。函数的返回类型为原组类型。
auto[name,string] name,string为自定义的变量。
#include "tuple"
std::tuple<std::string, int> returntest() {
return { "yzh",21 };
}
int main() {
//auto [name, age] = returntest();
auto [name, age] = std::tuple<std::string, int>( { "yzh", 99 });
std::cout << name;
return 0;
}
C++17新特性——如何处理optional(可选)数据?
optional为可选,意味着当函数返回值是optional类型时,可以返回存在或不存在(也就是空),同时,又可以在其存在时通过,value()拿到具体的返回值。这在文件解析时非常有效果。
其中value_or(数据); 则可以在为空时设置我们想要的数据。
比如
std::optional<std::string> val=value_or("not");
例子:
解析文本流
#include "optional"
#include "fstream"
std::optional<std::string> ReadFile(const std::string& textpath) {
std::ifstream stream(textpath);
if (stream) {
//解析文本数据
stream.close();
return "content"; //假设是我们所读取到的具体数据。
}
else {
return {};
}
}
int main() {
std::optional<std::string> date = ReadFile("date.txt");
std::string val=date.value_or("not");
std::cout << val << std::endl;
if (date) {
std::cout << "success\r\n";
std::cout << date.value();
}
else{
std::cout << "fail\r\n";
}
return 0;
}
C++17新特性——单一变量存放多类型数据(varint)
它从实现上来说,帮你创建了一个类,这个类有你所放的许多类型的数据(充当类的成员变量),所以它的内存大小为所有类型的数据之和。这比用联合体(占用大小为最大数据类型的大小)更消耗内存;所有联合体从效率上更好的,但varint类型安全,不会造成未定义的行为。
头文件
#include "variant"
创建
std::variant<std::string,int ,float> var;
判断该变量属于那种类型 通过,index()方法 返回从0开始的正整数,对应放进去的类型顺序(从0开始)。或者是通过std::get_if<需要判断的类型>(指针) ,true返回一个所判断类型的指针,false则为null。
获取值
通过
std::get<类型>(varint变量>
int main() {
std::variant<std::string, int, float> a;
a = "yzh";
std::cout << std::get<std::string>(a) << "\n";
if (a.index() == 0) {
std::cout << "string\n";
}
if (auto value = std::get_if<std::string>(&a))
{
std::cout << "string\n";
}
a = 48;
std::cout<< std::get<int>(a) << "\n";
std::cout << a.index() << "\n";
return 0;
}
C++17新特性——用any存放任意类型的数据
头文件 #include "any" ,与上面所提的varint类似,但varint由于列出了所以类型的数据是类型安全的,而any可以放任意类型,任意大小的数据但并不类型安全,在取出数据时若判断出错则会抛出异常。
取数据,通过
std::any_cast<类型>(any变量);//转换失败则抛出异常
另外在VS,msvc平台上,若存放类型的数据<32字节,则std::any和varint一样,若>32字节,则会动态开辟内存,而动态内存开辟有性能消耗。
#include "any"
int main() {
std::any p;
p = 2;
int& p1 = std::any_cast<int&>(p);
std::cout << p1<<"\n";
p = std::string("hello");
std::string& p2 = std::any_cast<std::string&>(p);
std::cout << p2;
return 0;
}
使用多线程的方式让C++运行得更快
在C++的代码中,有些任务是能够不受制约并行执行的,这依赖于我们的硬件,而现在硬件大多都是多核心的,所以能够通过并行的方式榨干CPU性能使得程序更快。其中运行最多的领域就是游戏的资源加载了,多核并行加载更快。
头文件 #include "future"
std::async()
摘抄自知乎
C++中的std::async()详解 - 知乎 (zhihu.com)
功能:第二个参数接收一个可调用对象(仿函数、lambda表达式、类成员函数、普通函数......)作为参数,并且异步或是同步执行他们。
a、对于是异步执行还是同步执行,由第一个参数的执行策略决定:
(1)、std::launch::async 传递的可调用对象异步执行;
(2)、std::launch::deferred 传递的可调用对象同步执行;
(3)、std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;
(4)、如果我们不指定策略,则相当于(3)。
b、对于执行结果:
我们可以使用get、wait、wait_for、wait_until等待执行结束,区别是get可以获得执行的结果。如果选择异步执行策略,调用get时,如果异步执行没有结束,get会阻塞当前调用线程,直到异步执行结束并获得结果,如果异步执行已经结束,不等待获取执行结果;如果选择同步执行策略,只有当调用get函数时,同步调用才真正执行,这也被称为函数调用被延迟。
c、返回结果std::future的状态:
(1)、deffered:异步操作还没有开始;
(2)、ready:异步操作已经完成;
(3)、timeout:异步操作超时。
重点在于:若不接受该返回结果,其返回值将会被清理,在清理的时候,它需要确保过程实际上已经完成,这基本上意味着它根本就不是并行的,破坏了异步;所以在使用时必须得进行接受,若是在for循环中遍历则用std::future的数组进行接受。
在for循环遍历是接受(存储)std::future 类型的返回值。
std::vector<std::future<void>> futures;
futures.push_back(std::async(std::launch::async, test2, count));
原因:这个函数会在std::future的析构函数中等待传入的任务完成,
代码案例
#include "future"
#include "iostream"
#include "chrono"
#include "thread"
struct Timer
{
std::chrono::time_point<std::chrono::steady_clock>start, end;
Timer() {
start = std::chrono::high_resolution_clock().now();
}
~Timer() {
end = std::chrono::high_resolution_clock().now();
std::chrono::duration<float> duration = end - start;
std::cout << duration.count() * 1000.0f << "ms\n";
}
};
void test2(int count) {
for (int i = 0; i < count; i++);
}
std::vector<std::future<void>> futures;
void test(int count) {
for (int i = 0; i < 100; i++) {
//参数为函数 以及该函数的若干参数
futures.push_back(std::async(std::launch::async, test2, count));
//test2(count);
}
}
int main() {
using namespace std::literals::chrono_literals;
{
Timer t;
test(1000000);
}
return 0;
}
运行结果
多线程:2.2188ms
单线程:58.2778
在视图观察者下让C++的字符串更快
在创建字符串时,比std::string temp ="mmm" 会在堆上开辟空间,在字符串传参时有时也会调用字符串的隐式构造函数再次再堆上开辟内存,而这种动态开辟内存的方式则是比较慢的会影响性能。
如何在某些情况下让字符串更快?给我的感觉还是得分情况,如果在某些情形下只需要使用原本的字符串,这就是所说的观察,比如只需要显示字符串及其子串,这样只需要指针就可以做到。从而就不需要在堆上动态开辟内存。据此,C++17的新特性给出了字符串视图,它本质上还是const char *类型的指针。
std::string_view(指针,size) //size为该指针的偏移
案例代码
#include "pch.h"
#include "variant"
union myunion
{
myunion(int a) {
this->a = a;
}
myunion(float a) {
this->b = a;
}
myunion(const std::string & a) {
this->s = a;
}
~myunion() {
}
int a;
float b;
std::string s;
};
//#include "any"
static uint32_t creatcount = 0;
void* operator new(size_t size) {
creatcount++;
return malloc(size);
}
#define VAL 0
#if VAL
void test( const std::string & s)
{
}
#else
#endif
void test( std::string_view & t) {
std::cout << t << "\r\n";
}
int main() {
#if VAL
std::string temp = "yzhhhhhhhh";
std::string temp1 = temp.substr(0, 3); //子字符串
std::cout << temp1 << "\r\n";
std::string temp2 = temp.substr(3, -1); //子字符串
std::cout << temp2 << "\r\n";
//test("hello"); 调用隐式构造函数
std::cout <<"堆上开辟次数:"<< creatcount << "\r\n";
#else
const char* ptr = "hello world";
std::string_view ptr1(ptr, 5);
std::string_view ptr2(ptr+5, 2);
std::string_view ptr3(ptr+7, 1);
test(ptr1);
test(ptr2);
test(ptr3);
std::cout << "堆上开辟次数:" << creatcount << "\r\n";
#endif
return 0;
}
以上代码通过窗口监视都指向都一个字符串的地址。
C++的单例模式
单例模式,这意味着整个程序进程只有唯一一个该单例类的对象,单例类里可以放像C一样不受约束的函数,一般当我们想要重复使用一个对象(比如数据库代理执行增删查改的对象)则可以把该对象设计成单例。另外在类外不允许创建该类的其他对象。
class Single {
public :
Single(const Single&) = delete;
static Single & GetInstance() {
static Single instance;
return instance;
}
static void test() {
std::cout << "hello\r\n";
}
private:
Single() {
}
};
int main() {
Single::GetInstance().test();
Single& mSingled = Single::GetInstance();
mSingled.test();
return 0;
}
值得注意的是:与在Java语言不同,java中可以直接定义在类中静态成员变量,然后通过类名.的形式直接访问(假如是public static 修饰),并且可以赋初始值。而在c++中除了直接在类中定义外,还要在翻译单元进行声明(否则报错),且不能赋初值。 为了方便,可以在获取该单例的静态函数体内定义该单例。正如上面的代码一样。
public:
static Single & GetInstance() {static Single instance;
return instance;
}
C++的小字符串优化
在vs平台下,在debug(调试模式)下,创建字符串会在堆上开辟空间,使得程序不够快,而在release(发布模式)下,则会使用小字符串优化,当创建的字符串小于一定字节时(这个与编译平台有关),比如,在VS2019中。默认小于16个字节的字符串为小字符串,它们会使用栈缓冲区来存放而不会开辟内存,这会让程序更快;而大于或等于16个字节则会依然进行堆开辟内存。
跟踪内存分配的简单方法
可以用通过运算符重载,重载new 和delete方法,查看有堆上内存的分配情况。
#include "iostream"
#include "memory"
struct mMemory {
static int currentBytes;
static int freeBytes;
static int allcateBytes;
mMemory() {
currentBytes = freeBytes = allcateBytes = 0;
}
};
int mMemory::allcateBytes;
int mMemory::currentBytes;
int mMemory::freeBytes;
void* operator new(size_t size) {
std::cout << "allcating:" << size << "bytes" << "\r\n";
mMemory::allcateBytes += size;
return malloc(size);
}
void operator delete(void* memory, size_t size) {
std::cout << "freeing" << size << "bytes" << "\r\n";
mMemory::freeBytes+=size;
free(memory);
}
int main() {
{
{
std::cout << "current bytes:" << mMemory::allcateBytes - mMemory::freeBytes << "\r\n";
std::vector<int> vector1;
vector1.reserve(8);
std::cout << "current bytes:" << mMemory::allcateBytes - mMemory::freeBytes << "\r\n";
}
std::cout << "------------------------\r\n";
std::unique_ptr p3 = std::make_unique<int>();
std::cout << "current bytes:" << mMemory::allcateBytes - mMemory::freeBytes << "\r\n";
std::string p = "hello i am rookie";
std::cout << "current bytes:" << mMemory::allcateBytes - mMemory::freeBytes << "\r\n";
}
std::cout << "current bytes" << mMemory::allcateBytes - mMemory::freeBytes << "\r\n";
return 0;
}
结果分析:在debug模式下,不使用小字符串优化,默认会给一个字符串8个字节的空间,若不够则再给32个字节的空间。
C++的左值与右值以及左值引用与右值引用
左值:有地址的值,有某种存储支持的值,
右值:临时值。
左值引用,右值引用正如字面意思一样。
左值引用只能接受左值,右值引用只能接受右值。右值引用是一个左值,所以左值引用能够接受右值引用.
用const 修饰的左值引用两种情况都能接受,即能接受左值和右值,这种情况下编译器会创建一个临时变量,这时临时变量就成为了左值,而其值为临时值(右值) 。这时一种非常灵活的规则,C++中的很多库的接受参数类型的写法就是这种。
测试案例
void PrintVal(const int& val) {
std::cout <<"左,右值都接受" << val << "\r\n";
}
void PrintVal(int& val) { //左值引用只接受左值
std::cout <<"只接受左值" << val << "\r\n";
}
void PrintVal(int&& val) { //两个&&表示右值引用
std::cout <<"只接受右值" << val << "\r\n";
}
int& GetLeftVal() {
static int c = 2;
return c; //返回左值引用
}
int&& GetRight() { //这种写法我也是第一次写
return 245;
}
int main() {
int b = 2; //b是左值,2是右值
int& a = b; //左值引用只能接受左值
const int& c = 2; //这种情况被称为常引用,接受参数可以是左值也可以是右值
PrintVal(20);
PrintVal(b);
GetLeftVal() = 21; //左值引用接受右值
GetLeftVal() = b;//左值引用接受左值
GetLeftVal() = GetRight(); //左值引用接受右值引用
std::cout << GetLeftVal() << "\r\n";
return 0;
}
C++中参数的计算顺序
试想在一个这样的函数中:
void testparam(int a,int b){
std::cout << "a:" << a << "b:" << b << "\r\n";
}
int main() {
int a = 0;
testparam(a++, a++);
return 0;
}
输入会是多少? (在vs平台 )
release :a:0 b:0 msvc的编译器,C++14的标准
debug/release a:1 b:0 msvc的编译器,C++17的标准
而在GCC,clang编译器下则报出警告 value值未定义,然后得出的值也各不相同,没有统一的标准。
C++17(C++17新标准:后缀表达式必须要在其他表达式之前被计算,这规定了计算的顺序是一个接一个的被计算)。而如果在release模式下,有些值很有可能在编译时被计算,然后被替换掉。
答案是:参数的计算顺序是未定义的行为,不要使用。(因为跨平台会导致不同的结果)
C++的移动语义(右值的最大的一个用处)
在C++11(C++11引入了右值引用)以上的标准上,移动语义能让我们移动对象,移动对象而不重新创建对象,使性能更好。在很多情况下,我们传递参数难免要拷贝一份原来的对象,如果这份数据有成员变量需要堆开辟内存,那么性能将会受影响(为什么不用引用?如果用了引用就等于有多个"指针"指向同一块内存区域,需要考虑释放一个指针后,另一个指针变成无效指针的问题(貌似可以考虑用智能指针貌似也能解决这个问题。)有时我们并不愿意怎么做,只希望数据能以指针的方式进行移动。)
比如下面一个例子。
class String
{
public:
String() = default;
String(const char* str) {
size = strlen(str);
date = new char[size];
if (!date) {
printf("分配失败\r\n");
}
else {
memcpy(date, str, size);
}
printf("create\r\n");
}
String(const String& str){
printf("copied\r\n");
size = str.size;
date = new char[size];
memcpy(date, str.date, size);
}
String(String&& str) {
date = str.date;
size = str.size;
str.date = nullptr;
str.size = 0;
}
~String() {
printf("destroyed\r\n");
free(date);
}
void Print() {
for (int i = 0; i < size; i++)
printf("%c", date[i]);
printf("\n");
}
private:
int size;
char* date;
};
class Entity
{
public :
Entity(const String& src)
: mstring(src)
{
}
Entity( String&& src)
: mstring( std::move(src))
{
std::cout << "moved\r\n";
mstring.Print();
}
void PrintEntity() {
mstring.Print();
}
private:
String mstring;
};
int main() {
Entity en("hello");
en.PrintEntity();
std::cin.get();
return 0;
}
"hello"字符串会调用隐式构造函数String(const char * str) 后调用 Entiry(String && src) 完成数据的移动。 值得一提的是在初始化列表中
Entity( String&& src)
: mstring( std::move(src))
std::move() 等价于 (String && ) 强制转换成右值引用。
引用B站一条评论
移动运算符std::move()与移动构造函数与移动赋值操作符
移动运算符std::move()
它是标准库提供的一个库函数,好处在于使用不需要知道其类型就能转换成右值引用类型。
移动构造函数:
正如上面所提到的: 一个类的构造函数允许右值引用传参。在传参的同时,把临时值转换成右值引用,它们都是左值。
//移动构造函数
String(String&& str) {
if (this != &str) {
delete date;
date = str.date;
size = str.size;
str.date = nullptr;
str.size = 0;
std::cout << "调用移动构造函数\r\n";
}
}
移动赋值操作符
它来源于赋值操作符 “=”的重载。即为赋值运算符重载为移动赋值运算符。
赋值操作符等号在创建对象时,根据等号的另一边的类型隐式调用其构造函数。(值得一提的是,当在类内部重写了 = 运算符操作,那么在该类对象被定义时使用了该 = 赋值运算符,那么调用的会是其隐式构造函数而不是 赋值运算符的重载函数。)
为此,我有如下例子作为实践验证。
class String {
public:
String() = default;
String(const char* str) {
size = strlen(str) + 1;
date = new char[size];
memcpy(date, str, size);
}
String( const String& str) {
size = str.size + 1;
date = new char[size];
memcpy(date, str.date, size);
printf("create\r\n");
}
//移动构造函数
String(String&& str) {
if (this != &str) {
delete date;
date = str.date;
size = str.size;
str.date = nullptr;
str.size = 0;
std::cout << "调用移动构造函数\r\n";
}
}
String& operator=(String&& str) {
if (this != &str) {
delete[] date;
size = str.size;
date = str.date;
str.date = nullptr;
str.size = 0;
std::cout << "调用赋值运算符重载移动运算符\r\n";
}
return *this;
}
~String() {
delete []date;
std::cout << "destroyed\r\n";
}
void Print() {
for (int i = 0; i < size; i++) {
printf("%c", date[i]);
}
printf("\r\n");
}
private:
int size;
char* date;
};
int main() {
String apple = ("apple");
//String dest(std::move(apple)); //调用移动构造函数。
//String dest = std::move(apple); //在定义时调用移动构造函数
String dest;
std::cout << "dest:";
dest.Print();
std::cout << "apple:";
apple.Print();
//dest = std::move(apple); //调用赋值 = 运算符重载
std::cout << "dest:";
dest.Print();
std::cout << "apple:";
apple.Print();
//apple.operator=(std::move("apple"));
return 0;
}
在类内部重载赋值运算符 = 的情况下,验证了在类对象定义时使用 = 则调用是其隐式构造函数而不是移动运算符操作函数,而若该类对象已经被定义使用 =则调用移动运算符操作函数。
总结
C/C++深似海,写到后面越是感觉不够具体,不够详细,本篇算我入门之作吧。11月12日基础学习完结。
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器