基础语法
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++没有finally
标准 C++ 是没有类似 finally 这样的语句结构的。C# / Java 中保证无论是否出现异常,finally block 的代码均会得到执行;而 C++ 中,不论是否出现异常,局部变量的析构函数是会保证执行的,所以相对应与 finally block,C++ 的解决办法是 RAII。
在C++中通常使用RAII,即Resource Aquisition Is Initialization.就是将资源封装成一个类,将资源的初始化封装在构造函数里,释放封装在析构函数里。要在局部使用资源的时候,就实例化一个local object。在抛出异常的时候,由于local object脱离了作用域,自动调用析构函数,会保证资源被释放。
在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&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
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
比如
如果这里没有typename,SubType就会被当成一个static member,而 * 就被当成乘法了。
当要获得类的成员类型时,必须用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; }