基础语法
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;
} 


京公网安备 11010502036488号