基础语法

c++默认成员函数



c++中函数后加const

任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

常量引用(const &)

常量引用时既可接受lvalue又可接受rvalue
int &a=1;            //error
const int &a=1;    //ok
/*
相当于
const int temp = 1;
const int &a = temp;
*/
函数引用参数对比
void sink(double & r1);// matches modifiable lvalue
void sank(const double & r2); // matches modifiable&nbs***bsp;const lvalue, rvalue
void sunk(double && r3);// matches rvalue
如果我们既不想改变传入参数的值,也不想因为值传递产生太大的开销,那么可以使用常引用。

子类的转换为基类

直接赋值时,只取子类的基类部分;引用或指针的赋值方式也类似,只能访问子类中基类的地址空间。

子类和基类指针相互转换


基类-->子类用dynamic_cast比static_cast安全

理解隐藏
隐藏发生的主要原因,就是当子类有父类的同名成员(指成员变量和成员函数)时,子类对象访问该成员时,会发生冲突。所以编译器的处理方式是,优先考虑子类域中的自身成员。
理解覆盖
虚函数的目的是为了,在用父类指针指向不同的子类对象时,调用虚函数,调用的是对应子类对象的成员函数,即可以自动识别具体子类对象。直接用子类对象调用虚函数是没有意义的,一般情况也不会这样使用。
覆盖的情况下,子类虚函数必须与父类虚函数有相同的参数列表,否则认为是一个新的函数,与父类的该同名函数没有关系。但不可以认为两个函数构成重载。因为两个函数在不同的域中。
理解重载
重载必须是发生在同一个域中的两个同名不同形参之间的。如果一个在父类域一个在子类域,是不会存在重载的,属于隐藏的情况。调用时,只会在子类域中搜索,如果形参不符合,会认为没有该函数,而不会去父类域中搜索。
#include<iostream>
using namespace std;
class A{
    public:virtual void f(){cout<<'A'<<'\n';}
};
class B:public A{
    public: void f(){cout<<'B'<<'\n';}
};
class C:public A{
    public: void f(){cout<<'C'<<'\n';}
};
int main()
{
    A o1;B o2;C o3;
    o1.f();     o2.f();     o3.f();
    A *pa1=&o2;     A *pa2=&o3;
    pa1->f();
    pa2->f();
    return 0;
}
/*
输出
A
B
C
B
C
*/

关键字virtual

虚函数的底层机制:C++进阶之虚函数表
一个类只要有虚函数,编译器就会为它生成虚函数表,同时将虚函数表的起始地址保存在类的实例的前4个字节(32位系统),
所以虚函数不管有几个,都只占4字节的类存储空间。
调用虚函数时,先找到表地址,之后根据函数声明的先后顺序,找到表中的函数入口地址。

关键字override

描述:override保留字表示当前函数重写了基类的虚函数。

目的:1.在函数比较多的情况下可以提示读者某个函数重写了基类虚函数(表示这个虚函数是从基类继承,不是派生类自己定义的);2.强制编译器检查某个函数是否重写基类虚函数,如果没有则报错。

用法:在类的成员函数参数列表后面添加该关键字既可。
//例子:
class Base {
virtual void f();
};

class Derived : public Base {
void f() override; // 表示派生类重写基类虚函数f
void F() override;//错误:函数F没有重写基类任何虚函数
};

为什么c++需要把基类函数的析构函数设置为virtual

#include<iostream>
using namespace std;
class shape
{
    public:
    virtual void draw()=0;
    virtual 
    ~shape(){cout<<"shape die"<<'\n';}
};
class round:public shape
{
    public:
    void draw(){}
    ~round(){cout<<"round die"<<'\n';}
};
int main()
{
    shape *p=new round();
    delete p;
    return 0;
}
这个例子有两个前提:
1)基类指针指向子类
2)基类指针动态初始化
释放基类指针时,必须要调用子类的析构函数。
只有声明为virtual的父类析构函数,才能调用子类的析构函数。

虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

为什么C++没有finally

标准 C++ 是没有类似 finally 这样的语句结构的。C# / Java 中保证无论是否出现异常,finally block 的代码均会得到执行;而 C++ 中,不论是否出现异常,局部变量的析构函数是会保证执行的,所以相对应与 finally block,C++ 的解决办法是 RAII。
在C++中通常使用RAII,即Resource Aquisition Is Initialization.就是将资源封装成一个类,将资源的初始化封装在构造函数里,释放封装在析构函数里。要在局部使用资源的时候,就实例化一个local object。在抛出异常的时候,由于local object脱离了作用域,自动调用析构函数,会保证资源被释放。

父子类的构造及析构

父类比子类先构造,子类比父类先析构,像个栈
从多个继承时,根据声明的顺序初始化的
#include<iostream>
class fa
{
public:
    fa(){std::cout<<"i am father\n";}
    ~fa(){std::cout<<"kill father\n";}
};
class fa1
{
public:
    fa1(){std::cout<<"i am father1\n";}
    ~fa1(){std::cout<<"kill father1\n";}
};
class fa2
{
public:
    fa2(){std::cout<<"i am father2\n";}
    ~fa2(){std::cout<<"kill father2\n";}
};
class son : public fa1,public fa,public fa2
{
public:
    son(){std::cout<<"i am son\n";};
    ~son(){std::cout<<"kill son\n";};
};

int main()
{
    son s1;
    return 0;
}


/*
i am father1
i am father
i am father2
i am son
kill son
kill father2
kill father
kill father1
*/

C++拷贝构造函数详解


拷贝函数的调用时机
对象以值传递的方式传入函数参数
对象以值传递的方式从函数返回
对象需要通过另外一个对象进行初始化

拷贝函数的定义,参看C++拷贝构造函数
对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
类中可以存在超过一个拷贝构造函数。
X::X(const X&);        //是
X::X(X);        //不是
X::X(X&, int a=1);        //是
X::X(X&, int a=1, int b=2);        //是

深拷贝与浅拷贝

浅拷贝,函数参数是某一实例对象的引用,把实例对象的值依次赋给新的对象。
深拷贝,针对成员变量存在指针的情况,不仅仅是简单的指针赋值,而是重新分配内存空间
深拷贝时,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。(这时候大概必须手动写拷贝构造函数)
默认拷贝函数是浅拷贝。
具体例子参看C++拷贝构造函数

防止默认拷贝发生

通过对对象***的分析,我们发现对象的***大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数
甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。


explicit关键字

class A{
    public: A(int a){cout<<a<<endl;}
};
class B{
    public: explicit B(int b){cout<<b<<endl;}
};
int main(){
    A a = 5;
    B b(10);
    //B b = 15;
    return 0;
}

/*  输出:5 10
加上注释语句编译报错,不能隐式调用

为什么赋值语句会隐式调用构造函数?
类B有自定义构造函数,所以编译器不会自动生成编译器,
但会自动生成

析构函数
***构造函数
赋值函数
取址函数

B b = 15; 执行顺序如下:
"15"隐式转换为实例B,即调用B(int)构造生成临时实例t,变为B b = t;
临时实例t隐式调用***构造函数,把t***给b,t销毁。

*/

匿名namespace

当定义一个命名空间时,可以忽略这个命名空间的名称:
namespce {}
编译器在内部会为这个命名空间生成一个唯一的名字,而且还会为这个匿名的命名空间生成一条using指令。所以上面的代码在效果上等同于:
namespace _UNIQUE_NAME_ {}
using namespace _UNIQUE_NAME_;
命名空间都是具有external 连接属性的,只是匿名的命名空间产生的__UNIQUE_NAME__在别的文件中无法得到,这个唯一的名字是不可见的。

using的主要使用场景

配合命名空间,对命名空间权限进行管理
using namespace std;//释放整个命名空间到当前作用域
using std::cout;    //释放某个变量到当前作用域


类型重命名,作用等同typedef,但是逻辑上更直观

#include <iostream>

#define DString std::string    //! 不建议使用!

typedef std::string TString;   //! 使用typedef的方式

using Ustring = std::string;   //!使用 using typeName_self = stdtypename;

//更直观
typedef void (tFunc*)(void);
using uFunc = void(*)(void);

int main(int argc, char *argv[])
{

    TString ts("String!");
    Ustring us("Ustring!");    
    string s("sdfdfsd");

   cout<<ts<<endl;
    cout<<us<<endl;
    cout<<s<<endl;
    return 0;
}




继承体系中,改变部分接口的继承权限。
有这样一种应用场景,比如我们需要私有继承一个基类,然后又想将基类中的某些public接口在子类对象实例化后对外开放直接使用。如下即可
#include <iostream>
//#include <array>
#include <typeinfo>


using namespace std;

class Base
{
public:
    Base()
    {}
    ~Base(){}

    void dis1()
    {
        cout<<"dis1"<<endl;
    }
    void dis2()
    {
        cout<<"dis2"<<endl;
    }
};

class BaseA:private Base
{
public:
    using Base::dis1;//需要在BaseA的public下释放才能对外使用,
    void dis2show()
    {
        this->dis2();
    }
};

int main(int argc, char *argv[])
{

    BaseA ba;
    ba.dis1();
    ba.dis2show();

    return 0;
}


内存对


typename的常见作用之一,指出其后是一个类型名

比如typedef一个已经被typedef过的关键词时,就需要使用
/*
T::iterator *iter会被编译器解释为两个数相乘。
为了避免这种矛盾,需要用typename来指出这是一个类型名
*/
template <typename T> class Y
{
    typename T::iterator *iter;
    typedef typename T::iterator iterator; //定义了Y::iterator类型名称 
};




typename和class在c++模版中的区别

一般情况下typename和class可以互换,但是当需要表示某标识符是类型的时候用只能用typename而不能用class
当要获得类的成员类型时,必须用typename
比如
template <typename T>
class MyClass {
typename T::SubType * ptr;
};
有了typename,SubType就被当成了T中定义的一个类型;
如果这里没有typename,SubType就会被当成一个static member,而 * 就被当成乘法了。


delete和delete[]的区别

对于简单类型两者相同
对于class组成数组两者不同,要用delete[]对数组的每一个对象析构

c++ static成员函数和成员变量

static成员变量必须在类声明的外部初始化,static成员函数则没有这种限制

c++11中的forward

c++11中的forward有多种形式(重载)
std::forward<T>(u)有两个参数:T 与 u
当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值
比如
void f(int &&x)
{cout<<"r value\n";}
void f(int &x)
{cout<<"l value\n";} 

int main()
{
  int x=1;
  f(forward<int&>(x));
  f(forward<int>(x));
  return 0;
}
forward相当于有条件的move