C++原理

C++运算符的优先级和结合性

Blog. ImProgrammer

//一些典型应用场景,可以省去写括号
if (i >> k & 1);
cnt = cnt * 2 % mod;
cnt = (cnt + 2) % mod;
int mid = l + r >> 1;
//这里要注意,&的优先级是低于 ==,==是7,&是8,因此需要加括号才行
while (j && ((nums[j] & 1) == 0)) j -- ;

static关键字的使用

  1. 函数体内的static作用域是当前函数,表示该变量在下次这个函数调用时,不重新分配,还是上次执行之后的值。
  2. 在模块内(一个cpp文件之内)的static全局变量,可以被模块内的所有函数访问,但是模块外的其他函数无法访问。(全局静态变量)。全局和静态不同,全局变量在另一个cpp里可以使用extern来引入,而静态变量不允许被其他cpp引入。
  3. 在模块内的static函数只能被当前模块的其他函数调用,外部无法调用。函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。
  4. 类中的static成员变量属于整个类所有,对类的所有对象,调用都是同一个。
  5. 类中的static成员函数属于整个类所有,函数不接受this指针,因此只能访问static成员变量。

总结:静态的变量和函数都是将变量或者函数限定在了当前cpp文件之中,局部则是多次调用复用。类的静态表示全类共有的东西。

C/C++ 中指针和引用的区别

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  3. 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  4. 作为参数传递时,指针需要被解引用(*操作)才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
  5. 可以有const指针,但是没有const引用;
  6. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变
  7. 指针可以有多级指针(**p),而引用只有一级;
  8. 指针和引用使用++运算符的意义不一样,指针是指向下一个地址,引用代表所引用对象值+1;
  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

智能指针

原理

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。unique_ptr是多个指针不能同时指向一个对象,shared_ptr可以指向同一个。weak_ptr不会改变shared_ptr的引用计数。

  1. auto_ptr:C98标准中,所有的智能指针都只能用auto_ptr来实现,在C++11中已经被弃用。
  2. unique_ptr:这种指针只能指向一个对象,不能多个指针同时指向一个对象,体现在只能转移而不能简单赋值,构造函数中也不能通过值来直接构造。而shared_ptr刚好是可以进行赋值,这样可以多个指针指向同一个对象。
  3. shared_ptr:C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
  4. weak_ptr:当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。(因为引用计数不会为0)为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

例子

//unique_ptr使用
#include <iostream>
#include <memory>

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout << "Task::Constructor" << std::endl;
    }
    ~Task() {
        std::cout << "Task::Destructor" << std::endl;
    }
};

int main()
{
    // 创建一个空指针
    std::unique_ptr<int> ptr1;
    // 通过原始指针创建 unique_ptr 实例
    // 构造unique_ptr时用指针作为构造参数
    std::unique_ptr<Task> taskPtr(new Task(23));

    //通过 unique_ptr 访问其成员
    int id = taskPtr->mId;
    std::cout << id << std::endl;

    return 0;
}

//上述代码的输出
//Task::Constructor
//23
//Task::Destructor

//shared_ptr使用
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
    int i;
    A(int n):i(n) { };
    ~A() { cout << i << " " << "destructed" << endl; }
};
int main()
{
    shared_ptr<A> sp1(new A(2)); //A(2)由sp1托管,
    shared_ptr<A> sp2(sp1);       //A(2)同时交由sp2托管
    shared_ptr<A> sp3;
    sp3 = sp2;   //A(2)同时交由sp3托管
    cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl;
    A * p = sp3.get();      // get返回托管的指针,p 指向 A(2)
    cout << p->i << endl;  //输出 2
    sp1.reset(new A(3));    // reset导致托管新的指针, 此时sp1托管A(3)
    sp2.reset(new A(4));    // sp2托管A(4)
    cout << sp1->i << endl; //输出 3
    //这里体现了引用计数为0时执行delete,从而执行析构函数
    sp3.reset(new A(5));    // sp3托管A(5),A(2)无人托管,被delete
    cout << "end" << endl;
    return 0;
}

//程序输出
//2,2,2
//2
//3
//2 destructed
//end
//5 destructed
//4 destructed
//3 destructed

c++11 weak_ptr使用

虚函数

原理

在父类派生子类的时候,如果某一个方法父类希望子类在定义的时候重新实现,应该用virtual关键字将该方法定义为虚函数。如果子类重新实现了该方法,同样需要加上virtual关键字,或者在C++11标准中不使用virtual关键字,而在函数定义后边加上override关键字。使用虚函数可以使得程序有更好的扩展性,并且虚函数是实现多态的一种方法(另外重载也是,但是是静态实现)。

虚函数实现了动态绑定,在编译阶段无法确定当前调用对象使用的是哪个版本的虚函数,只有到运行阶段才能够得知。实现方法是使用虚函数表虚函数表指针。其中在类中声明虚函数时,类会产生一个虚函数表,将所有虚函数的地址都放入其中,当需要的时候可以查表得到当前虚函数的地址,实现调用。而虚函数指针是每一个对象会隐式保存一个,如果父类的指针指向子类的时候,在调用虚函数时会首先查找虚函数指针,找到对应类的虚函数表,再调用对应类的虚函数,这样就可以确切知道是调用哪一个版本了。另外虚函数指针是一个确实的指针,因此在sizeof的时候,对象会多4字节(存放虚函数表地址)用来存放虚函数表指针。

内联函数、构造函数和静态成员函数无法声明为虚函数,其中内联函数是在编译时确定的,和动态绑定冲突;构造函数在调用的时候,父类和子类的概念还没有出现,因此不能声明为虚函数;静态成员函数只与类有关系,和对象绑定给谁无关,因此不行。

当多次继承时,虚函数每次都会被继承,如果不想继续让其子类继续继承当前的虚函数,可以使用final声明,之后再继承的类就无法继续重新实现该函数了。

例子

//C++ primer中的例子
class Quote {
public:
    Quote() = default;
    Quote(const std::string &book, double sales_price):bookNo(book), price(sales_price){}
    std::string isbn() const { return bookNo;}
    virtual double net_price(std::size_t n) const { return n * price;}
    virtual ~Quote() = default; //继承关系的根节点一般会声明一个虚的析构函数
private:
    std::string bookNo;
protected:
    double price = 0.0;
};

class Bulk_quote : public Quote {
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, std::size_t, double);
    //这里是虚函数的C++11规范继承
    double net_price(std::size_t) const override;
    //这样写也可以
    //virtual net_price(std::size_t) const;
private:
    std::size_t min_qty = 0;
    double discount = 0.0;
};

Blog.Ryan C++虚函数原理

Blog.zkfopen C++虚函数的作用和多态

new和malloc的区别

  1. new从自由存储区上为对象动态分配内存空间,而malloc从上分配。自由存储区是个更高一层抽象的概念,是一块计算机中的内部存储空间,有可能是堆,也可能是静态存储区,需要具体看operator new的具体实现。new甚至可以不分配空间,比如传入指针参数的new实现。事实上,new的实现可以基于malloc。
  2. new分配成功时,返回对象类型的指针,不用转换类型。malloc只是给了一块内存,类型是void*,大小是传入的参数,需要强制转换一下。
  3. new分类失败时,抛出bac_alloc异常,而malloc失败时,返回NULL。
  4. new运行时,先分配内存,之后调用构造函数,最后返回一个该对象指针。delete时先调用析构函数,之后再释放内存。而malloc更加底层,没有这样规范的全部操作。
  5. new和delete是运算符,可以进行重载。malloc和free不允许被重载。
  6. new支持数组类型的分配,比如如下代码段:
//new
A *ptr = new A[10]; //分配10个A对象
delete [] ptr;

//malloc
int *ptr = (int *) malloc(sizeof(int) * 10);

Blog.林嵩 new与malloc有什么区别

复制构造函数

当使用类创建对象时,如果我们也希望可以用一个已有的类对象初始化一个新的类对象,就如同简单类型直接赋值一样,这时就会调用复制构造函数。如果没有显式定义复制构造函数,编译器会自动隐式定义一个缺省的复制构造函数,一个inline+public的成员函数,形式是类名::类名(const 类名&)。

//基础类型的直接赋值初始化
double x = 5.0;
double y = x;

//类对象的复制构造函数
point pt1(2, 3);
point pt2 = pt1;
point pt2(pt1);

复制构造函数的三个被使用场景:

  1. 当把一个已经存在的对象赋值给另一个新的对象时;
  2. 实参和形参都是对象,进行形参和实参的结合时;
  3. 当函数返回值是对象,函数调用完成返回时;

另外对于复制构造函数,要注意深拷贝和浅拷贝的关系。当一个类中有指针时,如果仅仅使用默认的复制构造函数,会导致新创建的对象中的指针也指向对应内存。这样会导致两次析构函数调用,如果有delete语句,则会delete两次,引发错误。正确做法应该显式定义复制拷贝函数,这样可以在拷贝函数内部重新分配内存,并且重新赋值指针。

Blog.C++拷贝构造 函数详解

C++11新特性:右值引用

什么是左值和右值?

左值和右值原本是C语言用来区别“=”左右两边操作数的一个说法,但是在C++中引入了const关键字,从而某些位于等号左侧的量也无法被赋值,因此左值和右值被重新定义,左值代表能在系统中占据某一块内存空间有地址的变量和数值;右值代表在计算过程中的临时量,无法得到对应内存地址。其中对于持久的左值来说,C语言中就已经定义对于其的引用使用方法,相当于别名,即左值引用。但是对于右值,因为没有地址,无法持久保持当前值,所以没有右值引用的概念。

Blog.CSDN 理解C和C++中的左值和右值

右值引用

C++11的新标准中,引入了右值引用这一强大的特性,主要为了解决类对象的复制与移动的关系。C++11中使用语法&&来定义右值引用,可以对右值进行引用操作,具体实现不明。右值引用的其中一个意义是,可以实现“移动语义”,通过向移动赋值运算符重载的参数中直接传入右值引用,可以避免一次无意义的对象构造和析构过程。


庄敬日强,功不唐捐。