CPP学习日记---(一)
CPP学习日记---(一)
面向对象
构造函数&析构函数
构造函数和之前的java很像啊,也有多态的性质,可以同名不同参。(编译的时候方法签名不同)
但是这里的构造函数可以进行简化操作,例子如下:
class test{
public:
int val;
int num;
void setval(int _val){
this->val=_val;
}
// test(int a){
// this->val=a;
// }
test(int a,int b) : val(a),num(b){
//成员变量(构造函数参数)
}
这就是所谓的列表段初始化
然后我来解释一下析构函数
析构函数本身是一个成员函数,它只在对象生命周期结束时执行清理操作,如释放资源、关闭文件。
但是,这里的资源和文件不包括动态分配的内存(new
和malloc
)
因此一般来说,析构函数是不用定义的,编译的时候会自动添加一个默认的析构函数。但是如果在类对象中分配了相应的动态内存,那么是需要析构函数在实例被销毁时回收那部分资源,例子如下:
MyClass(int size) { // 构造函数
myArray = new int[size]; // 动态分配内存
}
~MyClass() { // 析构函数
delete[] myArray; // 释放动态分配的内存
}
记住,实例销毁要带上delete!!
tips:面经常考题---malloc和new的区别与联系
:两者都是动态分配内存
-
new 一个对象后会自动调用对象的构造函数,进行初始化。删除时也会自动调用析构函数。
malloc 仅分配内存,不需要构造函数,删除时也只需要free
-
new 是类型安全的,因为知道对象的类型,返回指向对象指针
malloc仅返回 void* 类型指针,需要显示转换为需要的类型
-
new 无法分配足够内存时,会抛出bad_alloc
malloc 分配内存失败时返回NULL
拷贝构造函数
简单来说就是通过对象来赋值给一个新的对象(深拷贝)
原理:在构造函数中将对象作为参数传入函数,从而生成一个新的对象
tips:什么是浅拷贝和深拷贝,他们和拷贝构造函数之间的关系是什么?
:两者都是拷贝构造函数的结果
(类内有指针变量)
-
浅拷贝:如果没有专门定义拷贝构造函数,编译器会自动生成一个,但对于指针变量,该函数实现的是浅拷贝方式,即拷贝指针的值(内存地址)
乍一看没什么问题,因为指针解引用之后得到的数据依然是正常的
但是在对象销毁时,如果两个对象同时指向了一个数据,那么第一个对象在释放地址上的内存时,第二个对象再次释放就会导致内存泄漏甚至程序崩溃
同理,此时一个对象修改了指针指向的数据,另一个对象的指针指向的数据也会改变
-
深拷贝:为了解决浅拷贝问题,我们需要自定义一个拷贝构造函数,来实现指针拷贝时,调用不同的指针来存储相同的数据
这样拷贝之后,两边的指针指向的数据就是相互独立的了
例子如下:
test(const test& other){ ptr = new int(*other.ptr); }
测试样例:
void setval(int _val){ this->ptr=new int(_val); } /*这里的 ptr必须new 一个 不然这个局部变量在函数调用结束时会被销毁,导致ptr成为悬空指针 不能直接使用 &_val 取地址,这样也违反了深拷贝的原理,会导致指向的地址相同 */ mytest->setval(6); auto another_test = new test(*mytest); another_test->setval(33); cout<<*mytest->ptr<<" "<<*another_test->ptr<<endl;
有时候浅拷贝看上去并没什么问题,两个对象同时delete也好像并没什么,但是这种行为是不可靠的,可能刚好释放的内存上并没有什么有用的数据。
在大型项目中这样是有严重隐患的
内联函数
看完概念感觉像宏定义一样,在编译的时候直接进行替代
内联函数的主要作用是为了减少函数调用产生的开销
原理:函数调用的国产中,要进行参数传递、栈帧创建与销毁等过程。
如果是小体积函数的频繁调用,可以使用内联函数(将函数体直接替换掉函数调用)
自己手写一个max之类的
tips:刚好想到sort函数如何实现根据对象(结构体)内部变量进行排序
std::sort(container.begin(),container.end(),[](const container &a, const container &b){
return a.val<b.val;
})
以上为Lambda表达式,[]为捕获子句,定义了表达式能获取的外部变量
lambda 表达式 是一种匿名函数,主要作用有:
简化代码
本地化逻辑(使用的地方直接编写)
加强STL
并发和异步编程(回调函数,捕获上下文中的变量,进行业务处理)
结合promise、future 等类
闭包(也就是捕获机制)
可以清楚看到依赖哪些变量、可以避免悬空引用,捕获列表的依赖变量在执行时依旧有效(值捕获)
或者自定义 pattern(bool 函数) ,作为sort的第三个参数
bool pattern(const container &a, const container &b){
return a.val<b.val;
}
tips:
所谓的悬空引用或者悬空指针,都值得是引用或者指向的源数据被释放或者销毁
面经题:值捕获和引用捕获的区别在哪
值捕获可以保证不受外部影响(类似深拷贝)
引用捕获会将引用的改变同步到原数据(小心悬空引用)
auto ref_capture = [mytest](){//值捕获
cout<<*mytest->ptr<<endl;
};
auto ref_capture = [&mytest](){//引用捕获
cout<<*mytest->ptr<<endl;
};
善良的博主找了悬空引用的例子,但是结果mingw出问题了,好像没包含thread、future等STD头文件,跑到stack overflow看了,找到了GitHub上官方提供的相应头文件才跑通,GPT还提醒我是编译选项的问题
auto ref_capture = [&mytest]() {
// 模拟延迟,增加悬空引用发生的可能性
this_thread::sleep_for(chrono::seconds(1));
cout << *mytest->ptr << endl; // 访问悬空引用的风险
};
auto future = async(launch::async, ref_capture);
delete mytest;
delete another_test;
future.get();
return 0;
友元函数
简单来说,将内部的一些私有或者保护变量额外开权限给外部的函数使用
作用还是比较多的,便于理解的有以下几点:
-
因为可以通过友元函数访问私有(或保护)变量,可以减少公共API的压力
-
两个类之间需要实现访问机制,被访问者可以设置友元函数,帮助访问者访问
但比较常用的,而且比较难理解的是运算符重载
就是重写运算符,为什么?
因为像正常的两个对象,是不能直接进行运算符操作的
例如我的test
类不可能直接进行两个类之间的加减,或者两个类之间的比较,亦或者直接cout<<mytest;
因此我们需要重载一下运算符,同时,为了实现内部的私有(或者保护)变量的运算,我们就要用到友元函数了,例子如下:
//类内声明
friend ostream &operator<<(ostream &output, const test &rect);
//类外定义
ostream &operator<<(ostream &output, const test &rect){
output<<"public: "<<rect.num<<"private: "<<rect.sval;
return output;
}
//调用(传入的是对象,不是指针)
cout<< *mytest <<endl;
//输出:public: 10private: 6666
tips:我们在引用的过程中经常遇到const,有什么用呢
-
引用中使用const主要是为了防止传入对象被修改
-
以及,使用const关键字可以同时接受常量和非常量对象(不带const只能接受非常量)
除了引用之外,还有别的地方可以使用,比如定义常量、
加在成员函数后面,具体实现前面:这样能表示该函数不会修改任何类的数据成员
修饰类内的常量成员(必须在初始化的时候定义好)
- 指针常量:指针不变,数据可变
- 常量指针:数据不变,指针可变
常量因为可以不用修改,所以可以放在只读内存段中,从而优化性能
再最后补充一个,引用和指针的联系
在很多编译器的底层实现中,其实引用就是一类特殊的指针
只不过引用必须初始化,不能为空;引用不能改变指向类型
总的来说,就是一种更安全的指针