c++日常杂记

const修饰函数

在类中将成员函数修饰为const表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。

HTTP协议实现断点续传

断点续传指的是下载传输文件可以中断,之后重新下载时可以接着中断的地方开始下载,而不必从头开始下载。断点续传需要客户端和服务端都支持。

原理是客户端一块一块的请求数据,最后将下载回来的数据块拼接成完整的数据。

1.客户端

发送http请求时发送请求头Rnge:bytes=x-x  表示需要请求的范围。

2.服务器端

返回http码206。(非断点传续返回200)

返回头有如下:

Accept-Ranges:bytes--表示服务器端支持断点续传

ETag--标识服务器的下载资源有没有改变(随资源内容一起变化)若改变了,说明资源内容已改变,客户端则应从头开始下载,而不是从中断处接着下载。

Last-Modified--也是标识服务器端资源最后一次改变的时间。

Content-Ranges:bytes x-x--返回的范围。

析构函数为什么是虚函数

基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。

智能指针及其作用

一、引入背景

使用 C++ 的指针可以动态开辟存储空间,但若在使用完毕后忘记释放(或在释放之前,程序 throw 出错误,导致没有释放),导致该内存单元一直被占据直到程序结束,即发生了所谓的内存泄漏
【注】内存泄漏是指堆内存的泄漏。堆,就是那些由 new 分配的内存块。
  因此智能指针的作用就是为了保证使用堆上对象的时候,对象一定会被释放,但只能释放一次,并且释放后指向该对象的指针应该马上归 0

二、C++11 以前的智能指针

auto_ptr,指向一个动态分配的对象指针,它的析构函数用于删除所指对象的空间,以此达到对对象生存期的控制。
  已被弃用,原因是:

  • 避免潜在的内存崩溃:auto_ptr 进行赋值操作时候,被赋值的取得其所有权,去赋值的丢失其所有权(变成空指针,无法再使用);
  • 不够方便:没有移动语义(后面讲)。

在C++中重载有一个特点,就是对于基类声明为virtual的函数,之后的重载版本都不需要再声明该重载函数为virtual。即使在派生类中声明了virtual,该关键字也是编译器可以忽略的。这带来了一些书写上的便利,却带来了一些阅读上的困难。

什么是函数对象(函数符)

函数对象也叫函数符,函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象

上面这句话的意思是指:函数名、指向函数的指针和重载了括号运算符的类对象与括号结合,从而以函数方式实现某种功能。

为什么需要函数对象

有2个原因:

  1. 函数对象可以有自己的状态,即利用它的成员变量来记录状态,这样可以实现一些复杂的功能。
    而函数没有办法记录状态,除非借助于全局变量。
    利用内部状态可以实现一些复杂的功能,比如,一个函数对象在多次的调用中可以共享这个状态。
    不过需要注意一点,在STL中都是传值的,所以函数对象也是作为一个值而被拷贝传入的;假如函数对象的内部状态在函数对象被调用时会发生改变,则很可能因为每次传入给调用者时,函数对象的内部状态都会被初始化而导致错误(见参考文献2中的例子)。所以,函数对象的使用者的行为最好不要依赖于函数对象的内部状态。当然,若函数对象的内部状态根本不会改变,那即使依赖也没有问题。

  2. STL中不少容器的模板参数都是函数对象,而不是函数指针,因此我们需要使用函数对象。
    例子有上面的 unordered_map 的模板参数,又比如,set的模板参数,priority_queue 的模板参数,等等。
    那么,为什么STL中要使用函数对象呢?为了组件技术中的可适配性(adapability),即将某些修饰条件加诸其上而改变状态。
    在侯捷的《STL源码剖析》的1.9.6节和第8章对此有所阐述。

C++ 函数指针 & 类成员函数指针

总结:类成员函数指针与普通函数指针不是一码事。前者要用 .* 与 ->* 运算符来使用,而后者可以用 * 运算符(称为"解引用"dereference,或称"间址"indirection)。普通函数指针实际上保存的是函数体的开始地址,因此也称"代码指针",以区别于 C/C++ 最常用的数据指针。而类成员函数指针就不仅仅是类成员函数的内存起始地址,还需要能解决因为 C++ 的多重继承、虚继承而带来的类实例地址的调整问题,所以类成员函数指针在调用的时候一定要传入类实例对象。

define中的三个特殊符号:#,##,#@

  1. #define Conn(x,y) x##y
  2. #define ToChar(x) #@x
  3. #define ToString(x) #x

(1)x##y表示什么?表示x连接y,举例说:

  1. int n = Conn(123,456); /* 结果就是n=123456;*/
  2. char* str = Conn("asdf", "adf"); /*结果就是 str = "asdfadf";*/

(2)再来看#@x,其实就是给x加上单引号,结果返回是一个const char。举例说:

char a = ToChar(1);结果就是a='1';
做个越界试验char a = ToChar(123);结果就错了;
但是如果你的参数超过四个字符,编译器就给给你报错了!

error C2015: too many characters in constant   :P

(3)最后看看#x,估计你也明白了,他是给x加双引号

char* str = ToString(123132);就成了str="123132";

模板类与模板成员函数不能分文件写,一般声明和定义都在.h文件里

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。

《C++编程思想》第15章(第300页)说明了原因:
模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

模板类序列化需要在类内部声明和实现输入输出友元函数

#include <iostream>
#include <ostream>
#include <istream>
#include <fstream>

template<class T>
class TP
{
public:
    TP();
    TP(int a,char* s):m(a),str(s){}
    friend std::ostream& operator<<(std::ostream& x,const TP<T>& a)
    {
        x<<"模板类TP"<<typeid (T).name()<< a.m<<a.str;
        return x;
    }
    friend std::istream& operator>>(std::istream& x,TP<T>& a)
    {
         x>>a.m>>a.str;
         return x;
    }
    friend std::ofstream& operator<<(std::ofstream& x, TP<T>& a)
    {
        x<<a.m<<a.str;
        return x;
    }
    friend std::ifstream& operator>>(std::ifstream& x, TP<T>& a)
    {
        x>>a.m>>a.str;
        return x;
    }
    int m;
    char* str;
};

拷贝构造函数与移动构造函数的区别:

  1. 拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用
  2. 拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。

C++exit退出程序那些内存会被释放

全局变量、全局静态变量、局部静态变量会被释放,其它的对象不会被析构,程序里的指针如果不手动调用delete都不会调用析构;

通过函数指针调用虚函数列表中的虚函数

#include "iostream"
using namespace std;

class CTest
{
public:
 inline CTest():m_nVal(2){};
 inline virtual void test(){cout << "ctest::test1" << endl;}
 inline virtual void test2(){cout << "CTest:: test2!" <<endl;}
private:
 int m_nVal;
};
class cSon:public CTest
{
};
typedef void (*pfun)();  //定义函数指针
int main()
{

    CTest test;
    cSon son;
    //下面的代码可以证明子类中有指向VTABLE表指针的存在;
    int num = 0;
    num = sizeof(test);  //   num = 4;字节 ,就是m_nVal的内存大小;
    num = sizeof(son);  //   num = 8;  字节,因为基类有虚函数,所以子类多了指向虚函数表的指针(4字节)和 m_nVal的内存大小;

    cSon * pSon = new cSon;      //pson这一个指针地址(最前面的一个指针)可以看成是保存虚函数表地址的地址
    void * pvtable = (void*)*(unsigned long*)pson; //把pSon指针转换成地址指针,然后用*取地址对应的值(也就得到vtable的地址)
    void * pVal = (void*)*(unsigned long*)pvtable; //用*取pvtable地址里对应的值;
    fun f = (fun)pVal;    //转成函数指针  
    f();  //调用CTest::test()函数; 
    delete pson;
    return 0;

}

满足POD条件的数据有什么用途?

  (1).可以使用字节赋值,比如memset,memcpy操作

  (2).对C内存布局兼容。

  (3).保证了静态初始化的安全有效。

模板中标明“内嵌依赖类型名”

class MyArray 
{ 
  public:
  typedef int LengthType;
  .....
}

template<class T>
void MyMethod( T myarr ) 
{ 
  typedef typename T::LengthType LengthType; 
  LengthType length = myarr.GetLength; 
}

一、多态的概念      

      多态就是多种形态,C++的多态分为静态多态与动态多态。

静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。

动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

C++多态虚函数表详解(多重继承、多继承情况)_青城山小和尚-CSDN博客_c++多重继承有几个虚函数表

C++实现智能指针

原理:对象浅拷贝,实现对引用计数(同一块内存)的读写

template<class T>
class SmartPtr{
public:
    SmartPtr(T* t){
        ptr = t;
        refcount = (unsigned int*) malloc(sizeof(unsigned int));
        (*refcount)=1;
    }
    SmartPtr(const SmartPtr& sp){
        ptr = sp.ptr;
        ++(*sp.refcount);
        refcount = sp.refcount;
    }
    ~SmartPtr(){
        (*refcount)--;
    }
    void operator=(const SmartPtr& sp){
        ptr = sp.ptr;
        ++(*sp.refcount);
        refcount = sp.refcount;
    }
    T* get(){
        return ptr;
    }
    T& operator*(){
        return *ptr;
    }
    T* operator->(){
        return ptr;
    }
    unsigned int count(){
        return *refcount;
    }
    void clear(){
        delete  ptr;
        ptr = nullptr;
        *refcount = 0;
        free(refcount);
    }
private:
    unsigned int* refcount{nullptr};
    T* ptr{nullptr};
};
int main()
{
    int * pw = new int(9);
    SmartPtr<int> pw1(pw);
    SmartPtr<int> pw2(pw1);
    {
        SmartPtr<int> pw3 = pw2;
        SmartPtr<int> pw4 = pw2;
        cout<<pw1.count()<<pw2.count()<<*(pw2.get());
    }
    cout<<pw1.count()<<pw2.count()<<*(pw2.get());
    return 0;
}

C&C++设置缓冲区大小的三种方法

cout.setf(std::ios::unitbuf);
setbuf(stdout, NULL);
setvbuf(stdout, NULL , _IONBF, 4);

c++类型的兼容性 是什么概念?请举例说明

C++中的类型兼容性是指两个数据类型是否可以安全地互相转换或赋值。这个概念可以从几个方面来理解:

  1. 基本数据类型的兼容性: 对于基本数据类型,比如int, float, double等,C++定义了隐式类型转换规则。较小的数值类型可以自动转换为较大的数值类型(例如,从 int 到 double),而转换为较小的类型时可能会损失精度(例如,从 double 到 int)。

    int i = 42; 
    double d = i; // 自动类型转换,没有精度损失
    double d = 3.14; 
    int i = d; // 自动类型转换,但精度损失(小数部分丢失)
  2. 指针类型的兼容性: 在指针类型中,兼容性依赖于它们所指向的数据类型。通过C++中的强制转换操作符如 static_cast 和 reinterpret_cast,可以在不同指针类型之间进行转换,但安全性不能保证。

    int* iptr; char* cptr = reinterpret_cast<char*>(iptr); // 可能不安全的转换

  3. 类类型的兼容性: 在面向对象编程中,类的兼容性依赖于它们在继承层次结构中的关系。一个基类指针可以指向派生类对象(向上转型),这是安全的。

    class Base {};
    class Derived : public Base {};
    
    Derived d;
    Base* b = &d; // 安全的向上转型

    但反过来,将基类的指针或引用强制转换为派生类的指针或引用(向下转型),可能是不安全的,除非确保这种转换是合适的。

    Base* basePtr = new Derived();
    Derived* derivedPtr = static_cast<Derived*>(basePtr); // 如果basePtr实际上指向Derived对象,则安全
  4. 模板类型的兼容性: 在模板编程中,类型兼容性也很重要。例如,模板类或函数可以用不同的类型实例化,而这些类型必须符合模板参数的要求,否则会编译错误。

    template<typename T>
    void print(const T& val) {
        std::cout << val << std::endl;
    }
    
    print<int>(42); // int是兼容T的
    print<std::string>("hello"); // std::string也是兼容T的

在总结这些概念时,类型兼容性可以视为类型之间可以安全互换的能力,这依赖于类型转换的上下文和目的。这也是C++作为一种强类型语言的特点,它需要类型之间转换时保持类型安全性,避免隐式转换造成的意外错误。使用C++时应谨慎处理类型转换,并尽量利用C++提供的类型转换操作符来保证代码的安全性和清晰性。

c++里修饰函数、类构造函数和类成员函数的关键字都有哪些? 

修饰构造函数的关键字:

  1. explicit: 防止构造函数被用作隐式类型转换。仅适用于单一参数构造函数或多参数构造函数(C++11及之后),要求显式调用构造函数。

    class Foo {
    public:
        explicit Foo(int x) {
            // ...
        }
    };
    
    Foo obj = 123; // 错误,因为构造函数是explicit
    Foo obj(123);  // 正确
  2. default (C++11): 显式告诉编译器使用默认的构造函数,而不是删除它。

    class Foo {
    public:
        Foo() = default; // 使用编译器生成的默认构造函数
    };
  3. delete (C++11): 用于禁止构造函数,防止任何形式的对象构建。

    class Foo {
    public:
        Foo(int x) = delete; // 不能使用int参数构造Foo对象
    };
  4. noexcept (C++11): 指示构造函数不抛出异常。

    Foo() noexcept {
        // 构造函数不会抛出异常
    }

修饰成员函数的关键字:

  1. const: 用于成员函数声明的结束,表示成员函数不会修改类的成员变量(除了被mutable修饰的)。

  2. static: 表示成员函数不依赖于类的实例。

  3. virtual: 使成员函数可以在派生类中被重写,以支持多态。

  4. override (C++11): 显式指定已重写基类中的虚函数。

  5. final (C++11): 指定虚函数不可在任何派生类中被进一步重写。

  6. inline: 建议编译器内联成员函数。

  7. constexpr (C++11): 指定函数可以用于构造编译时的常量表达式。

  8. noexcept (C++11): 指示成员函数不抛出异常。

以上关键字可以用来修饰C++中的构造函数和成员函数,以便在不同场景下精确控制它们的行为。不过,并不是所有关键字都能同时用于构造函数和普通成员函数,一些关键字只能用于特定类型的函数。例如,override 只适用于成员函数,而不适用于构造函数,因为构造函数不可以被继承。同理,explicit 在C++中也仅用于单个参数的构造函数或具有多个参数的构造函数,它不适用于其他类型的成员函数。

SOLID 原则有哪些?对应的设计模式都有哪些?

S - 单一职责原则 (Single Responsibility Principle)

  • 定义: 一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。
  • 目的: 降低类的复杂度,提高可读性和可维护性。
  • 相关设计模式:
    • 策略模式 (Strategy Pattern): 将算法封装到独立的策略类中,使得算法可以独立于使用它的客户端而变化。
    • 装饰器模式 (Decorator Pattern): 动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。

O - 开放/封闭原则 (Open/Closed Principle)

  • 定义: 软件实体(类、模块、函数等等)应该对扩展开放,对修改封闭。
  • 目的: 允许在不修改现有代码的情况下添加新功能,从而提高可维护性和可复用性。
  • 相关设计模式:
    • 观察者模式 (Observer Pattern): 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
    • 模板方法模式 (Template Method Pattern): 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

L - 里氏替换原则 (Liskov Substitution Principle)

  • 定义: 子类对象应该能够替换掉程序中的父类对象,而程序逻辑不变。
  • 目的: 确保使用继承时,子类不会破坏父类的行为,从而保证代码的健壮性。
  • 相关设计模式:
    • 工厂方法模式 (Factory Method Pattern): 定义一个用于创建对象的接口,但让子类决定将哪一个类实例化。工厂方法模式让类的实例化延迟到其子类。
    • 抽象工厂模式 (Abstract Factory Pattern): 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

I - 接口隔离原则 (Interface Segregation Principle)

  • 定义: 客户端不应该被强迫依赖于它们不使用的方法。
  • 目的: 避免接口过于臃肿,提高接口的灵活性和可复用性。
  • 相关设计模式:
    • 适配器模式 (Adapter Pattern): 将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
    • 代理模式 (Proxy Pattern): 为其他对象提供一种代理以控制对这个对象的访问。

D - 依赖倒置原则 (Dependency Inversion Principle)

  • 定义: 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
  • 目的: 解耦代码,提高代码的可测试性和可维护性。
  • 相关设计模式:
    • 依赖注入 (Dependency Injection): 不自己创建依赖对象,而是通过构造函数参数、属性设置等方式,从外部传入依赖对象。
    • 控制反转 (Inversion of Control): 将对象的创建和管理交给外部容器来完成,而不是在对象内部控制。

需要强调的是,设计模式只是实现 SOLID 原则的工具,而不是目的。在实际开发中,应该根据具体情况选择合适的原则和模式,避免过度设计。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值