C++Primer(中文第五版)学习笔记(第5天)

通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员 标记出来,using声明语句中名字的访问权限由该using声明语句前的访问说明符决定。using声明语句是令某个名字在当前作用域内可见。

class Base {
public:
    int num; //基类中声明一个公有数据成员
    double price;
    void func() const ();
    void func(int) {};
    int func() const {};
};
class Derived: public Base {
private:
    using Base::num; //将派生类中继承基类的公有的num成员修改为private
public:
    double price; //隐藏了同名的基类成员(重用基类定义)  
    using Base::func; //把基类中名为func函数的所有重载实例添加到派生类的作用域中
};

使用struct和class关键字定义的类之间唯一的差别在于默认成员访问符和默认派生类访问说明符(struct均是默认public,class默认均是private)。

类中函数调用的解析过程(p->mem()或p.mem()):
1. 首先确定p的静态类型。
2. 在p的静态类型对应的类中查找mem。若找不到,则一次在直接基类中不断查找直到到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器报错。
3. 一旦找到了mem,就进行常规的类型检查以确定对于当前找到的mem,本次调用是否合法。
4. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

  • 若mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据的是对象的动态类型。
  • 反之,若mem不是虚函数或我们是通过对象进行的调用,则编译器将产生一个常规函数调用。

注意在不同的作用域中无法重载函数名,内层作用域中声明的函数名称将隐藏外层作用域中声明的同名实体(尽管形参列表不一致)

定义在派生类中的函数也不会重载其基类中的成员,若派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使形参列表不一致,基类成员也会被隐藏掉。

struct Base { int func(); };
struct Derived:Base { int func(int); };
Base b; Derived d;
b.func(); //正确,调用的是Base::func
d.func(); //错误,参数列表为空的Base::func被隐藏了
d.Base::func(); //正确,调用的是Base::func
d.func(10); //正确,调用的是Derived::func

一个基类总是需要虚析构函数! 且此时我们并不一定需要拷贝和赋值操作。

class Base {public: virtual ~Base() =default;}; //动态绑定析构函数

派生类继承基类构造函数的方式是提供了一条注明了基类名的using声明语句:

class Derived:public Base {
public:
    using Base::Base;//使用基类的构造函数
};

与上面提到了using的用法不同,当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数即一个形参列表完全相同的构造函数。另外一个构造函数的using声明不会改变该构造函数的访问级别。且一个using声明不能指定explicit或constexpr,这意味着若基类的构造函数是explicit或constexpr的,则继承的构造函数也拥有相同的属性。

当一个基类构造函数含有默认实参时,这些实参并不会被继承,相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。

若基类含有几个构造函数,派生类会继承所有这些构造函数,除以下两种情况:

扫描二维码关注公众号,回复: 2885530 查看本文章
  • 派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数不会被继承,定义在派生类的构造函数将替换继承而来的构造函数。
  • 默认、拷贝和移动构造函数不会被继承。编译器会默认合成。

注意我们不能把具有继承关系的多种类型的对象直接存放在容器中,一般来说,我们实际上存放的应当是基类的智能指针。

模版

template<typename T,class U=int> //类型参数,两个关键字含义相同,"=int"表示默认实参为int
template<unsigned N> //非类型参数,N必须是一个常量表达式,被实例化时N被该常量表达式替代。

当编译器遇到一个模版定义时,它并不生成代码,只有当我们实例化出模版的一个特定版本时,编译器才会生成代码。这意味着,为了生成一个实例化版本,编译器需要掌握函数模版或类模版成员函数的定义。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。

模版支持友元:

//前置声明,在将模版的一个特定实例声明为友元时用到
template <typename T> class Pal;
class C {
    friend class Pal<C>; //用类C实例化的Pal是C的一个友元
    tempalte<typename T> friend class Pal2; //模版类Pal2的所有实例都是C的友元,无须前置声明
    friend T; //将访问权限授予用来实例化C的类型
}

模版支持static:模版类的每个static数据成员必须有且仅有一个定义,但类模版的每个实例都有一个独有的static对象,且static成员函数只有在使用时才会实例化。

模版内不能重用模版参数名。若我们希望使用一个模版类型参数的类型成员,就必须显示告诉编译器该名字是一个类型。

typename T::type p; //定义一个T::type类型的变量p

显示模版参数:

template <typename T1,typename T2,typename T3>
T1 sum(T2,T3); //T1在最前,必须显示指定;T2,T3可函数实参推断而来
auto res=sum<long>(i,d); //long sum(int,double)
T3 sum2(T2,T1); //T3在最前,必须全部显示指定
auto res2=sum2<long,int,double>(i,d); //long sum2(int,double)

尾置返回:

template <typename T>
auto func(T bef,T end) ->decltype(*beg) { //返回类型为*beg参数类型
    return *beg;
}

特殊工具与技术

typeid(e)运算符获取表达式e的类型,得到一个常量对象的引用,该对象的类型是标准库类型type_info或其公有派生类。

if(typeid(*p)==typeid(Derived)){} //p实际指向Derived对象

枚举:当前枚举成员的值等于之前枚举成员的值加1,第一个默认为0

enum class color {red,yellow,blue}; //限定作用域的枚举类型
enum color {red,yellow,blue}; //不限定作用域的枚举类型,类名可不要
enum value:unsigned long {num1,num2,num3}; //将默认int整型表示的枚举成员改为unsigned long类型

枚举成员是const,初始值必须是常量表达式,每个枚举成员本身就是一个常量表达式

声明成员指针:

const string Base::*p; //指向一个Base类的(常量非常量)string成员的指针
p=&Base::data; //初始化成员指针
auto p=&Base::data; //最简单的声明并初始化成员指针的方法

嵌套类:定义在另一个类内部的类。嵌套类是一个独立的类,与外层类没有任何关系。在嵌套类的对象中不包含任何外层类定义的成员,外层类的对象中也包含任何嵌套类定义的成员。但嵌套类可以直接使用外层类的成员,外层类不能使用嵌套类的。可以声明static成员。

局部类:定义在一个函数内部的类。局部类只能访问外层作用域定义的类型名,静态变量以及枚举成员。若局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。不允许声明static。

static关键字说明
1. static变量(无论全局、局部、类)在全局/静态内存区分配内存。
2. 静态变量只在第一次声明时初始化,未经初始化的静态变量会被程序自动初始化为0。
3. 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的,也就是说该变量无法被extern。
4. 静态局部变量具有全局寿命,只第一次进入函数时被初始化,但只在局部作用域内可见。
5. 静态函数不能被其它文件所用。
6. 静态数据成员定义时要分配空间,不能在类内中定义,类体外定义不含static,由类的所有对象所共有。
7. 静态成员函数不与任何的对象相联系,不具有this指针,故而它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。类体外定义不含static。不能将静态成员函数定义为虚函数。

在main函数前执行的代码:在main函数前定义全局变量、对象和静态变量、对象的空间分配和赋初值,在main前执行的函数。

全局对象的构造函数会在main 函数之前执行

在main函数后执行的代码:释放空间、释放资源使用权等操作。

全局对象的析构函数会在main函数之后执行

int func() {cout <<"func()" << endl; return 0;} //注意这里的返回值不能是void
class A {
public:
   A() {cout << "constructor" << endl;}
    ~A() {cout << "destructor" << endl;}
};
A a; //先实例化a
int x=func(); //然后执行函数,这里不能没有返回值,否则直接报错不能执行该语句
int main(void){
     cout << "main" << endl;
    return 0;
}

运行结果:

constructor //按照实例化顺序先声明的a,执行A的构造函数
func() //然后执行的是fun函数
main //然后执行main函数
destructor //最后main函数后销毁空间

链接指示extern “C”:
链接指示对整个声明都有效。

extern "C" void (*pf)(int) //pf指向一个C函数,该函数接受一个int并返回void
extern "C" void f1(void(*)(int)); //f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" typedef void func(int); void f2(func *); //f2是一个C++函数,该函数的形参是指向C函数的指针

new和malloc的区别
1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件cstdlib支持。
2. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸即需要n*sizeof(int)。
3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4. new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
5. new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)
6. C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
7. new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
8. new[]与delete[]来专门处理数组类型,使用new[]分配的内存必须使用delete[]进行释放。malloc就给你一块原始的内存。
9. 使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/79776028
今日推荐