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){
        //成员变量(构造函数参数)
    }

这就是所谓的列表段初始化


然后我来解释一下析构函数

析构函数本身是一个成员函数,它只在对象生命周期结束时执行清理操作,如释放资源、关闭文件。

但是,这里的资源和文件不包括动态分配的内存(newmalloc

因此一般来说,析构函数是不用定义的,编译的时候会自动添加一个默认的析构函数。但是如果在类对象中分配了相应的动态内存,那么是需要析构函数在实例被销毁时回收那部分资源,例子如下:

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只能接受非常量)

除了引用之外,还有别的地方可以使用,比如定义常量、

加在成员函数后面,具体实现前面:这样能表示该函数不会修改任何类的数据成员

修饰类内的常量成员(必须在初始化的时候定义好)

  • 指针常量:指针不变,数据可变
  • 常量指针:数据不变,指针可变

常量因为可以不用修改,所以可以放在只读内存段中,从而优化性能


再最后补充一个,引用和指针的联系

在很多编译器的底层实现中,其实引用就是一类特殊的指针

只不过引用必须初始化,不能为空;引用不能改变指向类型

总的来说,就是一种更安全的指针

文章作者: P4ul
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 打工人驿站
杂谈 c++
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝