文章目录
- 前言
- 特1:C++中的 const 分为编译时常量与运行时常量
- 特2:全局 const 变量的作用域仍然是当前文件
- 一、默认参数与无名形参
- 二、函数的重载
- 三、构造函数初始化列表
- 四、类的static成员和const成员
- 五、const 对象
- 六、类的作用域
- 七、friend 关键字
- 八、封闭类和继承中各构造函数和析构函数执行先后
- 九、继承与派生
- 十、多态 (polymorphism)
- 十二、抽像类与纯虚函数
- 十三、引用
- 十四、RTTI (Run-Time Type Identification)
- 十五、运算符重载(Operator Overloading)
- 十六、模板
- 十七、Exception
- 十八、copy构造函数
- 十九、四种类型转换关键字
- 二十、函数对象
- 二十一、Namespace
- 二十二、inline内联函数
- 二十三、函数的可变参数
- To be continue...
- 总结
前言
- 前有c++11后有c++17现在有c++20, 新特性在不断增加.
- 从这篇笔记出发,开始记录,本篇只包括c++的内容 不包含c++11及以上
- 跟进 C++11-C++17新特性介绍
- 笔者使用的是Clion + MinGW 开发环境
提示:以下是本篇文章正文内容,下面案例可供参考
特1:C++中的 const 分为编译时常量与运行时常量
>.初始式是常量表达式的 const 对象称为 编译时常量 否则称为 运行时常量
>.编译时常量编译阶段就执行值替换了 值替换的过程类似宏展开
特2:全局 const 变量的作用域仍然是当前文件
>.这和添加了static关键字的效果类似
一、默认参数与无名形参
- 一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值.
- 在给定的作用域中只能指定一次默认参数.
- 函数声明时 形参可以没有名字.
二、函数的重载
- 形参列表必须不同(个数不同、类型不同、参数排列顺序不同等).
- Overload resolution (重载决议)
>.编译器对实参到形参的转换规则
优先级 行为优先级 举例说明 精确匹配 不做类型转换,直接匹配 int 到 int 细微转换 从数组名到数组指针; 从函数名到指向函数的指针; 从非 const 类型到 const 类型。 类型提升后匹配 整型提升 从 bool、char、short 提升为 int; 从 char16_t、char32_t、wchar_t 提升为 int、long、long long。 小数提升 从 float 提升为 double。 使用自动类型转换后匹配 整型转换 从 char 到 long; short 到 long; int 到 short; long 到 char。 小数转换 从 double 到 float。 整数和小数转换 从 int 到 double,float; short 到 float; float 到 int; double 到 long。 指针转换 从 int * 到 void *。 - ambiguity (二义性)
>.指调用重载函数时产生的二义性情况
- 在同一优先级中有两个或两个以上符合的重载函数(单参)
- 类似田忌赛马(多参) 如果平手就会报错:i’m ambiguous,which one you want?
三、构造函数初始化列表
- 成员变量的初始化顺序只与它在类中声明的顺序有关,跟初始化列表中的顺序无关}
- 如果是派生类,则最先初始化基类的构造函数
class Test{ private: int m_a; int m_b; public: Test(int b): m_b(b), m_a(m_b){ }; //这会先执行 m_a(m_b) 从而把一个未知值赋值给 m_a }
四、类的static成员和const成员
-
static 成员变量
- 必须在全局数据区完成初始化.
- static 成员变量不占用对象的内存,而是在全局数据区分配内存,即使不创建对象也可以访问。
- 可以通过对象名访问,也可以通过类名访问,要遵循 private、protected 关键字的访问权限限制。
-
static 成员函数
- 静态成员函数只能访问静态成员 (编译器不会传 this 给它)
- 不管有没有创建对象,都可以调用静态成员函数。
- 可以通过类来调用,也可以通过对象来调用, 要遵循 private、protected 关键字的访问权限限制。
-
const 成员变量
- 定义时需要初始化 可用const修饰 static成员变量 使其能在类内初始化
-
const 成员函数
- 在声明和定义的时候在函数头的结尾加上 const 关键字
- 只能读取成员变量的值,而不能修改成员变量的值
-
自身类型的静态成员
#include <iostream> using namespace std; class B{ public: const static B static_self; double length {1.0}; double width {1.0}; double height {1.0}; B(double a,double b,double c):length{a},height{c} { }; B(double side):B(side,side,side) {}; //委托构造函数 static void show(){}; }; const B B::static_self(2.2); //必须在类外定义 类内声明 int main() { cout<<B::static_self.height<<endl; //2.2 }
也可以将它定义成非const 类型的这样就可以修改了
五、const 对象
- 一旦将对象定义为常对象之后, 该对象所有成员都不能被修改 (只读), 该对象不能调用非const 成员函数
因为不知道非const 函数内部是否会修改成员.
六、类的作用域
-
#include<iostream> using namespace std; class test{ public: typedef int* pInt; pInt show(pInt num); }; test::pInt test::show(pInt num){ cout<<num<<endl; return num; } int main(){ int count=5; test obj; obj.show(&count); return 0; }
七、friend 关键字
- friend 函数
- 在当前类声明 friend 函数 以访问当前类的所有成员。(无限制)
- 必须传当前类的指针给 friend 函数
- friend 类
- 在A类中声明B类为 friend 类, 那么B类可以通过指针无限制访问A类所有成员
- 具体如何访问,也是通过传A类指针
- friend的关系默认是单向的 如果需要双向 则需要在B类中也声明A类为 friend 类
- friend不能传递: A中声明B为friend类 B中声明C为friend类 , 那么 C和A没有friend关系
八、封闭类和继承中各构造函数和析构函数执行先后
- 封闭类(嵌套类)
>.一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类(enclosed class)
>.封闭类必须要在其构造函数初始化列表指明其成员对象如何初始化(有默认构造能力的成员对象可以不用指明)
-
构造: 封闭类对象生成时,先执行所有成员对象的构造函数(顺序与其声明次序一致),然后才执行封闭类自己的构造函数。
-
析构: 先构造的后析构
-
- 单继承情况下
>.派生类必须要在其构造函数初始化列表指明其成员对象和基类如何初始化(有默认构造能力的成员对象和基类构造函数可以不用指明)
- 构造: 先执行基类构造函数,再执行成员对象(假设有)构造函数(下面同理省略),最后执行派生类构造函数。
- 析构: 先构造的后析构
- 多继承情况下
- 构造: 按声明派生类时基类们出现的顺序执行基类构造函数,再执行派生类构造函数。
- 析构: 先构造的后析构
- 虚继承情况下
- 构造: 在最终派生类中不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数
- 析构: 先构造的后析构
九、继承与派生
>.继承与派生是一个概念,只不过站的角度不同
-
protected: 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
-
继承时的关键词限制
继承方式/基类成员 public成员 protected成员 private成员 public继承 public protected 不可见 protected继承 protected protected 不可见 private继承 private private 不可见 -
名字遮蔽: 只要同名 基类成员就会被派生类成员遮蔽(包括成员变量和成员函数),派生类中如果要用基类的同名成员,需要用域解析符指定 (产生名字遮蔽的函数默认是不会构成重载的)
-
多继承菱形继承二义性: 如果多个基类中有同名的成员(可以是继承来的), 派生类中需要用域解析符指定用哪个基类的
-
内存模型:
假设从左到右对应地址从小到大
- 一般继承下内存模型: A派生出B,B的内存模型是AB, 然后B派生出C,C的内存模型是ABC;
- 多继承下内存模型: C继承自A和B 按C声明时AB出现的顺序排列 即 ABC 或 BAC
- 虚继承下内存模型:
标红的即为虚基类
- A 虚派生出 B 那么在B的内存中A的数据放在最后面 即B A \color{red}{\sf{A}} A. 往后继承自B的, A的数据都放在最后.
- B 正常派生出 C 那么在C的内存中 BC A \color{red}{\sf{A}} A
- B 虚派生出 C 那么在C的内存中 C B A \color{red}{\sf{BA}} BA
- 对于虚继承,将派生类分为固定部分和共享部分,并把共享部分放在最后
- 虚基类表指针: 存储虚基类的偏移量, 我在gcc下测试结果十分奇怪:
- A派生B派生C,假设 B 虚继承自 A,那么各对象的内存模型如下图所示 (假设每个对象的每个成员都是8字节)
- 假设 同时 C 又虚继承自 B,那么各对象的内存模型如下图所示:
-
虚继承:
- 虚继承是在派生类声明时指定的,指明该虚基类在将来菱形继承时,最终派生类只保存一份虚基类的数据
这时最终派生类使用虚基类的成员已无二义性
- 虚基类构造函数是由最终的派生类调用的(如果当前活动类是最终派生类), 其他中间类对其的构造调用无效
- 虚继承是在派生类声明时指定的,指明该虚基类在将来菱形继承时,最终派生类只保存一份虚基类的数据
-
向上转型(将派生类赋值给基类):
- 通常是安全的 能赋的就赋值,没用的就丢弃 编译器会自动调整(如果是指针类型,会自动调整指向地址)
- 派生类的函数不能赋给基类,除非存在虚函数.
十、多态 (polymorphism)
- 虚函数:
虚函数和虚析构函数 只需要在声明时指定
- 一旦将基类中的某函数声明为虚函数,那么在向上转型中基类将能使用派生类中的与该函数原型一致的函数
>.原型一致指 函数名和参数列表和返回值类型都相同 但有一种例外: covariance of return type
class A{ public: virtual A& f() {cout<<"A f"<<endl;A a;} private: }; class B:public A{ public: B& f() { cout<<"B f"<<endl;B b;} private: };
- 有了虚函数,基类指针指向基类对象时就使用基类的成员 指向派生类对象时就使用派生类的成员。
- 你可以只将基类中的函数声明为虚函数,这样所有派生类中原型一致的函数都将自动成为虚函数。
- 一旦将基类中的某函数声明为虚函数,那么在向上转型中基类将能使用派生类中的与该函数原型一致的函数
- 虚析构函数:
- 在向上转型中, 为了使当前基类能够执行派生类的析构函数 必须声明基类的的析构函数为虚析构函数
-
#include <iostream> #include <windows.h> using namespace std; //基类 class Base { public: Base() { str = new char[100]; cout << "Base constructor" << endl; }; virtual ~Base() { delete[] str; cout << "Base destructor" << endl; }; protected: char *str; }; //二代 class Derived : public Base { public: Derived() { name = new char[100]; cout << "Derived constructor" << endl; }; ~Derived() { delete[] name; cout << "Derived destructor" << endl; }; private: char *name; }; //三代 class Derived1 : public Derived { public: Derived1() { name1 = new char[100]; cout << "Derived1 constructor" << endl; }; ~Derived1() { delete[] name1; cout << "Derived1 destructor" << endl; }; private: char *name1; }; int main() { SetConsoleOutputCP(65001); // Base:初代 //Derived:二代 //Derived1:三代 cout << "---------初代指向二代----------" << endl; Base *pb = new Derived(); delete pb; cout << "---------二代指向二代----------" << endl; Derived *pd = new Derived(); delete pd; cout << "--------二代指向三代-----------" << endl; Derived *pb1 = new Derived1(); delete pb1; cout << "--------初代指向三代-----------" << endl; Base *pb2 = new Derived1(); delete pb2; cout << "-------------------" << endl; }
- virtual, 编译器发现包含了用这个关键字修饰成员函数的类的指针后 执行虚函数时候编译器会忽略指针原本的类型,而优先根据指针的指向来选择函数, 这同样适用于虚析构函数.这样上面的析构函数都能得到执行.
- 把上面virtual移动到二代析构函数前再运行,可以发现virtual是向后代影响的,这和虚函数一样.
内存泄漏时,程序会自动退出,可以把泄漏的语句注释掉
- 虚函数表:
- 如果一个类包含了虚函数,那么在创建对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table)
-
#include <cstdio> #include <iostream> #include <string> #include <windows.h> using namespace std; void func() { cout<<"--------------------"<<endl; cout << "OuterFunction" << endl; } //People类 class People { public: People(char *name, int age); ~People(); protected: void (*pf)(); char *m_name; int m_age; public: virtual void display(); //用virtual 修饰类的成员函数或析构函数时 类的内存模型上会多一个指针 放在其首地址上 virtual void eating(); }; People::People(char *name, int age) : m_name(name), m_age(age) { pf = func; } People::~People() { cout << "我终于被destructor了" << endl; } void People::display() { cout << "Class People:" << this->m_name << "今年" << m_age << "岁了。" << endl; } void People::eating() { cout << "Class People:我正在吃饭,请不要跟我说话..." << endl; } //Student类 // class Student : public People // { // public: // Student(string name, int age, float score); // public: // virtual void display(); // virtual void examing(); // protected: // float m_score; // }; // Student::Student(string name, int age, float score) : People(name, age), m_score(score) {} // void Student::display() // { // cout << "Class Student:" << m_name << "今年" << m_age << "岁了,考了" << m_score << "分。" << endl; // } // void Student::examing() // { // cout << "Class Student:" << m_name << "正在考试,不要打扰啊" << endl; // } // //Senior类 // class Senior : public Student // { // public: // Senior(string name, int age, float score, bool hasJob); // public: // virtual void display(); // virtual void partying(); // private: // bool m_hasJob; // }; // Senior::Senior(string name, int age, float score, bool hasJob) : Student(name, age, score), m_hasJob(hasJob) {} // void Senior::display() // { // if (m_hasJob) // { // cout << "Class Senior:" << m_name << "以" << m_score << "的成绩从大学毕业了,并且顺利找到了工作,Ta今年" << m_age << "岁。" << endl; // } // else // { // cout << "Class Senior:" << m_name << "以" << m_score << "的成绩从大学毕业了,不过找工作不顺利,Ta今年" << m_age << "岁。" << endl; // } // } // void Senior::partying() // { // cout << "Class Senior:快毕业了,大家都在吃散伙饭..." << endl; // } int main() { SetConsoleOutputCP(65001); typedef void (*mypf)(); //函数指针 类型 typedef mypf** cxk; //指向一个(指向函数指针的指针)的 指针类型 typedef void (*lbw)(People *); People *p = new People("赵红888", 29); p->display(); // cout << sizeof(string) << endl; //string 32字节 cout << sizeof(People) << endl; //内存自动对齐 依8的倍数补齐 所以我前面放的是占8个字节的成员 //从这里可以看出 用virtual修饰类的成员函数或析构函数时 类的内存模型首地址上会多出个二维指针 cxk类型 cout << (char *)*((cxk)p + 2) << endl; //打印出 赵红888 ((mypf) * ((cxk)p + 1))();//执行 OuterFunction //这个虚函数用到了成员 所以需要传p进去 (*((lbw)**(cxk)p))(p); //成功访问 解引用得到虚函数表的地址 再解引用得到第一个元素(就是第一个函数指针) (**(*(cxk)p + 1))(); //成功访问 解引用得到虚函数表的地址 滑动到第二个元素(第二个函数指针) return 0; }
十二、抽像类与纯虚函数
- 纯虚函数:
- virtual 返回值类型 函数名 (函数参数) = 0;
后面的=0只是一个标识作用
- virtual 返回值类型 函数名 (函数参数) = 0;
- 抽像类:
- 包含纯虚函数的类以及只要后代没有完成父类全部纯虚函数的定义的类都称为抽象类(Abstract Class).
- 抽象类不能被实例化.
- 抽象类通常是作为基类, 让派生类去实现纯虚函数, 派生类必须实现全部纯虚函数才能被实例化.
十三、引用
int a = 5; int &b = a ;
本质就是给原来的"容器"起了一个alias,容器的地址是不变的(容器a和b)
>.或者说是给原来的变量起了个小名,用这个小名和用原来的变量名是一样的 参数传递时和它初始化时同理
int &c = b;
意思是再起一个别名,abc引用的都是同一块数据- 它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度一样
- 必须在声明时初始化,只能引用左值 (除非是const 引用)
- 不能引用临时数据和常量 (除非是const 引用)
- const &:既能引用左值也能引用右值 编译器会为常量或临时数据生成一个无名变量来存储
十四、RTTI (Run-Time Type Identification)
- 前面讲了虚继承虚函数会在对象内存中增加指针, 现在还有 type_info 指针,如果声明了虚函数,那么实例化时就会有这个指针
- dynamic_cast、typeid 和异常调度程序 都借助RTTI
- gcc下的type_info位置 就是VFT[-1] 即虚函表地址往左滑一个指针的字节, 再解引用得到type_info结构的地址
输出 6People .#include <cstdio> #include <iostream> #include <string> #include <windows.h> using namespace std; //People类 class People { public: People(char *name, int age); ~People(); protected: void (*pf)(); char *m_name; int m_age; public: virtual void display(); //用virtual 修饰类的成员函数或析构函数时 类的内存模型上会多一个指针 放在其首地址上 }; People::People(char *name, int age) : m_name(name), m_age(age) { pf = func; } People::~People() { cout << "People destructor了" << endl; } void People::display() { cout << "Class People:" << this->m_name << "今年" << m_age << "岁了。" << endl; } int main() { SetConsoleOutputCP(65001); typedef long long** cxk; // People *p = new People("zhaoHong888", 29); //解引用得到虚函数表的地址,-1 得到 type_info指针 的地址 const type_info **pTypeInfoAddr=(const type_info **)(*(cxk)p-1); cout<<(*pTypeInfoAddr)->name()<<endl; return 0; }
- 参考
- RTTI pdf 文档
- c++内存布局、typeid、RTTI、dynamic_cast原理、虚函数调用原理
- openrce.org
- 结构图
十五、运算符重载(Operator Overloading)
- 运算符重载的格式为:
返回值类型 operator运算符名称 (形参表列){
//TODO:
} - 能够重载的运算符包括:
+ - * / % ^ & | ~ ! = < > += -= = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ – , -> -> () [] new new[] delete delete[]
- 不能重载的:
sizeof :? . ::
- 只能以成员函数重载的:
-> [] 函数调用运算符() =
- 重载不能改变运算符原本的优先级和结合性
- 将运算符重载函数作全局函数时,二元操作符需要两个参数, 一元操作符需要一个参数, 而且其中必须有一个参数是对象, 好让编译器区分这是程序员自定义的运算符
- 将运算符重载函数作成员函数时, 二元操作符需要一个参数, 一元操作符不需要参数
- 如果有现成的转换规则 则可能隐式转换 !
- 隐式转换
- 把其他类型转换为当前活动类类型
#include <iostream> using namespace std; //复数类 class Complex{ public: Complex(): m_real(0.0), m_imag(0.0){ } Complex(double real, double imag): m_real(real), m_imag(imag){ } //转换构造函数 如果出现double类型 ,可能会通过调用此函数转换为Complex类型 Complex(double real): m_real(real), m_imag(0.0){ } public: friend Complex operator+(const Complex &c1, const Complex &c2); public: double real() const{ return m_real; } double imag() const{ return m_imag; } private: double m_real; //实部 double m_imag; //虚部 }; //全局范围 重载+运算符 Complex operator+(const Complex &c1, const Complex &c2){ Complex c; c.m_real = c1.m_real + c2.m_real; c.m_imag = c1.m_imag + c2.m_imag; return c; } int main(){ Complex c1(25, 35); Complex c2 = c1 + 15.6; //等于 operator+(c1, Complex(15.6)) //如果是在类范围内重载+ 则下面这行会报错:no match for 'operator+' (operand types are 'double' and 'Complex') Complex c3 = 28.23 + c1;//等于 operator+(Complex(28.23), c1) cout<<c2.real()<<" + "<<c2.imag()<<"i"<<endl; cout<<c3.real()<<" + "<<c3.imag()<<"i"<<endl; return 0; }
- 把当前活动类类型转换成其他类型 及转换规则和 explicit 修饰符
#include <iostream> using namespace std; class Complex { double real, imag; public: //阻止了隐式转换的可能 (即double类型自动转换为Complex类型) explicit Complex(double r = 0, double i = 0) : real(r), imag(i){}; //阻止了隐式转换的情况 但增加了Complex类型可以强制转换为double型的规则 explicit operator double() { return real; } bool operator==(Complex rhs) { return (real == rhs.real && imag == rhs.imag) ? true : false; } }; int main() { //如果一个类有一个可以用单个参数调用的构造函数,那么这个构造函数就变成了转换构造函数 Complex c(1.2, 3.4); cout << (double)c << endl; //输出 1.2 用explicit修饰后 需要明确的进行类型转换 // double f=c; //被 explicit 阻止 需要明确的对c进行类型转换 if (c == (Complex)5.5)//执行Complex(5.5) explicit阻止了隐式转换 所以这里用了强制类型转换 { cout << "same" << endl; } else { cout << "No same" << endl; }; }
- 为避免隐式转换造成不可预料的后果或二义性 可用 explicit 好好调教
- 把其他类型转换为当前活动类类型
十六、模板
-
函数模板:
- 发生函数调用时编译器会根据传入的实参来推演形参的值和类型.
函数声明和定义都要带上模板头
-
template <typename T , typename T , …> 返回值类型 函数名(T &a, T &b){
//在函数体中可以使用类型参数 (上面的 T)
} //类型参数符号 可以自定义 只不过是个占位符而已 编译器根据实参推演出类型后会替换掉他们的位置 - 在传参时的自动转换仅能进行「const 转换」和「数组或函数指针转换」
- 当函数形参是引用类型时,数组不会转换为指针。
- 「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >
- 显式地指明实参时可以应用正常的类型转换
- 函数模板也可以重载
- 发生函数调用时编译器会根据传入的实参来推演形参的值和类型.
-
类模板:
-
//声明
template<typename T1 , typename T2> class Point{
//TODO:
};
//定义
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;TODO:
} - 与函数模板不同的是,类模板在实例化时必须显式地指明数据类型
- 不能重复定义同名的类模板(即使模板原形不同), 所以有了后面的显示专门化以满足需求
-
-
显示专门化(Explicit Specialization)
- 函数模板显示专门化
#include <iostream> #include <string> #include <windows.h> using namespace std; typedef struct { string name; int age; float score; } STU; //函数模板========================声明========================= template <class T> const T &Max(const T &a, const T &b); //函数模板的显示具体化(针对STU类型的显示具体化) //写法一: 因为下方都指出来了所以<>里是空的 template <> const STU &Max<STU>(const STU &a, const STU &b); //写法二: template <> const STU &Max(const STU &a, const STU &b); //经测试模板函数也能和普通函数构成重载 // const STU Max(const STU &a, const STU &b); //重载<< ostream &operator<<(ostream &out, const STU &stu); int main() { SetConsoleOutputCP(65001); int a = 10; int b = 20; cout << Max(a, b) << endl; STU stu1 = {"王明", 16, 95.5}; STU stu2 = {"徐亮", 17, 98.7}; cout << Max(stu1, stu2) << endl; return 0; } //函数模板============================定义=========================== template <class T> const T &Max(const T &a, const T &b) { return a > b ? a : b; } //写法一 这次形参指明了数据类型 template <> const STU &Max<STU>(const STU &a, const STU &b) { return a.score > b.score ? a : b; } //写法二 //template <> //const STU &Max(const STU &a, const STU &b) //{ // return a.score > b.score ? a : b; //} //经测试模板函数也能和普通函数构成重载 // const STU Max(const STU &a, const STU &b) // { // return a.score > b.score ? a : b; // } ostream &operator<<(ostream &out, const STU &stu) { out << stu.name << " , " << stu.age << " , " << stu.score; return out; } /*回顾一下前面学习到的知识,在 C++ 中 ,对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本, 在调用函数时,显示具体化优先于常规模板,而非模板函数优先于显示具体化和常规模板。*/
- 类模板显示专门化
#include <iostream> using namespace std; //类模板 template<class T1, class T2> class Point{ public: Point(T1 x, T2 y): m_x(x), m_y(y){ } public: T1 getX() const{ return m_x; } void setX(T1 x){ m_x = x; } T2 getY() const{ return m_y; } void setY(T2 y){ m_y = y; } void display() const; private: T1 m_x; T2 m_y; }; template<class T1, class T2> //这里要带上模板头 void Point<T1, T2>::display() const{ cout<<"x="<<m_x<<", y="<<m_y<<endl; } //类模板的显示具体化(针对字符串类型的显示具体化) template<> class Point<char*, char*>{ public: Point(char *x, char *y): m_x(x), m_y(y){ } public: char *getX() const{ return m_x; } void setX(char *x){ m_x = x; } char *getY() const{ return m_y; } void setY(char *y){ m_y = y; } void display() const; private: char *m_x; //x坐标 char *m_y; //y坐标 }; //这里不能带模板头template<> 如果是部分显示具体化 则要带上模板头 void Point<char*, char*>::display() const{ cout<<"x="<<m_x<<" | y="<<m_y<<endl; } int main(){ ( new Point<int, int>(10, 20) ) -> display(); ( new Point<int, char*>(10, "东京180度") ) -> display(); ( new Point<char*, char*>("东京180度", "北纬210度") ) -> display(); return 0; }
- 部分显示专门化
#include <iostream> using namespace std; //类模板 template<class T1, class T2> class Point{ public: Point(T1 x, T2 y): m_x(x), m_y(y){ } public: T1 getX() const{ return m_x; } void setX(T1 x){ m_x = x; } T2 getY() const{ return m_y; } void setY(T2 y){ m_y = y; } void display() const; private: T1 m_x; T2 m_y; }; template<class T1, class T2> //这里需要带上模板头 void Point<T1, T2>::display() const{ cout<<"x="<<m_x<<", y="<<m_y<<endl; } //类模板的部分显示具体化 //通过这里的模板头和类名后的尖括号里的类型编译器可以知道哪个模板参数具体化了 template<typename T2> class Point<char*, T2>{ public: Point(char *x, T2 y): m_x(x), m_y(y){ } public: char *getX() const{ return m_x; } void setX(char *x){ m_x = x; } T2 getY() const{ return m_y; } void setY(T2 y){ m_y = y; } void display() const; private: char *m_x; //x坐标 T2 m_y; //y坐标 }; template<typename T2> //这里需要带上模板头 void Point<char*, T2>::display() const{ cout<<"x="<<m_x<<" | y="<<m_y<<endl; } int main(){ ( new Point<int, int>(10, 20) ) -> display(); ( new Point<char*, int>("东京180度", 10) ) -> display(); ( new Point<char*, char*>("东京180度", "北纬210度") ) -> display(); return 0; }
- 函数模板显示专门化
-
非类型参数:
- 只能是一个整数/字符,或者是一个指向对象或函数的指针(也可以是引用)
- 数组的传参:
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){}
通过传数组名能自动推断出数组元素的个数也就是N的值 - 实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。
-
#include <iostream> using namespace std; template <class Z,int* N> Z test(Z val) { cout<<N<<endl; return val; } int temp = 5; int main() { //各个版本对非类型参数的要求略有不同 c++98要求必须将temp声明在全局, //即使用static修饰也无济于事 直到c++17 (至少gcc下如此) test<int,&temp>(97); return 0; }
-
模板特性:
- 模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”
- 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码;
- 模板的实例化是由编译器完成的,而不是由链接器完成的;
>.在多文件编程中这可能会导致在链接期间找不到对应的实例;
-
显示实例化:(多文件编程)
>.显示实例化后 就可以把模板的声明和定义分开到不同的文件中了
- 本质是告诉编译器要实现哪个版本的函数或类, 因为编译当前文件时照顾不到其他文件
template void Swap(double &a, double &b);
(Swap是一个函数模板,此针对double&类型显示实例化定义)
声明写法: 在前面加 extern 即可
template class Point<char*, char*>;
(Point是一个类模板)
声明写法: 在前面加 extern 即可
- 显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。
- 这和显示专门化作用的不同之处在于 显示专门化针对特定类型要定义有别于原模板的实现
-
类模板和模板类的概念
- 类模板: 类型参数还未知 看作是一个模板
- 模板类: 类型参数已经定义 看作是一个带模板的类
-
模板的继承与派生
- 类模板派生类模板
template <class T1, class T2> class A { Tl v1; T2 v2; }; template <class T1, class T2> class B : public A <T2, T1> { T1 v3; T2 v4; }; template <class T> class C : public B <T, T> { T v5; }; int main() { B<int, double> obj1; C<int> obj2; return 0; }
- 模板类派生类模板
template<class T1, class T2> class A{ T1 v1; T2 v2; }; template <class T> class B: public A <int, double>{T v;}; int main() { B <char> obj1; return 0; }
- 类模板派生类模板
-
模板参数的作用域:
#include <iostream> #include <string> using namespace std; template <class T1, class T2> class Pair { private: T1 key; //关键字 T2 value; //值 public: Pair(T1 k, T2 v) : key(k), value(v) { }; bool operator < (const Pair<T1, T2> & p) const; //如果用T1 T2 那将报错,declaration of template parameter 'T1' shadows template parameter template <class T3, class T4>//函数模板作友元 friend ostream & operator << (ostream & o, const Pair<T3, T4> & p); }; template <class T1, class T2> bool Pair <T1, T2>::operator< (const Pair<T1, T2> & p) const { //“小”的意思就是关键字小 return key < p.key; } template <class T1, class T2>//这就是模板的作用域 ostream & operator << (ostream & o, const Pair<T1, T2> & p) { o << "(" << p.key << "," << p.value << ")"; return o; } int main() { Pair<string, int> student("Tom", 29); Pair<int, double> obj(12, 3.14); cout << student << " " << obj; return 0; }
-
类模板与友元
#include <iostream> using namespace std; template<class T> class A { public: void Func(const T & p) { cout << p.v; } }; template <class T> class B { private: T v; public: B(T n) : v(n) { } template <class T2> friend class A; //把类模板A声明为友元 }; int main() { B<int> b(5); A< B<int> > a; //用B<int>替换A模板中的 T a.Func(b); return 0; }
-
类模板中的静态成员
#include <iostream> using namespace std; template <class T> class A { private: static int count; public: A() { count ++; } ~A() { count -- ; }; A(A &) { count ++ ; } static void PrintCount() { cout << count << endl; } }; template<> int A<int>::count = 0; template<> int A<double>::count = 0; //不同的模板类中的静态成员是不同的内存 int main() { A<int> ia; A<double> da; // A<float> f; error: undefined reference to `A<float>::count' ia.PrintCount(); da.PrintCount(); return 0; }
十七、Exception
-
一旦出现异常程序会自动退出,而使用了异常处理,可以对这种情况进行补救
#include <exception>// 好像不加也没什么 #include <iostream> #include <stdexcept> #include <string> using namespace std; class Base { }; class Derived : public Base { }; int main() { //catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。 //注意指针不能自动由const*转换到非const指针 try { // 发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。 // 所以检测到异常后 其后面的代码都不会被执行 char *p = NULL; throw 5; //当抛出异常后 捕捉不到 则交给系统处理终止程序 // throw(string) "exception"; // throw(const) 5; // throw "Unknown Exception"; throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象 cout << "This statement will not be executed." << endl; } catch (exception &a) { //C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。 //C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)你可以不throw 捕获所有的标准异常 //exception类具体来说就是标准库对一些类东西做了封装 比如当对某个自定义动态数组做越界取值 就throw exception 并根具具体情况传入构造参数 cout << "Exception type: int" << endl; } catch (int e) //如果不需要用到 也可以不写参数名 { cout << "Exception typechar int :" <<e<< endl; } catch (char const *e) { cout << "Exception typechar char const* :" <<e<< endl; } catch (string &e) { cout << "Exception typechar string" <<e<< endl; } catch (Base) { //匹配成功(向上转型) cout << "Exception type: Base" << endl; } catch (Derived) { cout << "Exception type: Derived" << endl; } //================================标准库抛出的异常====================== string str = "http://c.biancheng.net"; int a[9] = {0}; int b = 5; try { char ch2 = str.at(100); cout << ch2 << endl; } catch (exception &e) { //exception类位于<exception>头文件中 cerr << e.what() << endl; cout << e.what() << endl; } return 0; }
-
Exception specification
- 有些教程也称为异常指示符或异常列表.
- 异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能多或者少.
- 如果函数会抛出多种类型的异常,那么可以用逗号隔开:(函数声明)
double func (char param) throw (int, char, exception);
- 如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到.
double func (char param) throw ();
- 多态时派生类异常规范只能比基类限制更多(少一个参数),
- 后来的 C++11 已经将它抛弃了,不再建议使用。
十八、copy构造函数
- 就是说传参或者作函数返回值的过程会调用copy构造函数,但现代编译器没了,会优化掉不必要的临时对象
- gcc 编译器c++14及以下 添加
-fno-elide-constructors
参数 阻止对此的优化 - 前面用到的给对象赋值=就是默认给对象成员一一赋值,那如果有指针成员指向动态分配的内存?
每实例化一个对象其该指针成员都指向同一块内存,这时就必须定制copy构造函数. - 需要至少用引用作参数,因为如果传当前类的对象,那么实参到形参的传递就会调用构造函数会死循环
#include <iostream> #include <string> using namespace std; class Student{ public: Student(string name = "", int age = 0, float score = 0.0f); //普通构造函数 Student(const Student &stu); //拷贝构造函数 public: Student & operator=(const Student &stu); //重载=运算符 void display(); private: string m_name; int m_age; float m_score; }; Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ cout<<"Default constructor was called."<<endl; } //拷贝构造函数 Student::Student(const Student &stu){ this->m_name = stu.m_name; this->m_age = stu.m_age; this->m_score = stu.m_score; cout<<"Copy constructor was called."<<endl; } //重载=运算符 Student & Student::operator=(const Student &stu){ this->m_name = stu.m_name; this->m_age = stu.m_age; this->m_score = stu.m_score; cout<<"operator=() was called."<<endl; return *this; } void Student::display(){ cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl; } Student func(Student stud){ cout<<"+++++++++++++++++++++++++++++++"<<endl; Student s("小明", 16, 90.5); //调用默认 cout<<"------------------------------"<<endl; return s;//不调用 } int main(){ //里面Student()调用一次默认 赋值给stu时不调用 相当于把函数返回的当成stu Student stu = func(Student()); cout<<"=========================="<<endl; //stu1、stu2、stu3都会调用普通构造函数Student(string name, int age, float score) Student stu1("小明", 16, 90.5); Student stu2("王城", 17, 89.0); Student stu3("陈晗", 18, 98.0); //初始化中的赋值操作不会调用赋值运算符 Student stu4 = stu1; //调用拷贝构造函数Student(const Student &stu) stu4 = stu2; //调用operator=() stu4 = stu3; //调用operator=() Student stu5; //调用普通构造函数Student() stu5 = stu1; //调用operator=() stu5 = stu2; //调用operator=() return 0; }
十九、四种类型转换关键字
-
关键字 说明 static_cast 用于良性转换,一般不会导致意外发生,风险很低。 const_cast 用于 const 与非 const、volatile 与非 volatile 之间的转换。 reinterpret_cast 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。 dynamic_cast 借助 RTTI,用于类型安全的向下转型(Downcasting)。 - https://c.biancheng.net/view/2343.html
二十、函数对象
-
#include <iostream> #include <windows.h> using namespace std; #define OUTSCREEN(msg, ...) printf(msg, __VA_ARGS__) void c(int a) { cout << "我是普通的函数" << endl; } class Complex { double real, imag; public: Complex(double r = 0, double i = 0) : real(r), imag(i){}; double operator()(const char *str) //函数对象 { cout << real << str << imag << endl; return real + imag; } operator double() { return real; } //重载强制类型转换运算为double型 }; int main() { SetConsoleOutputCP(65001); Complex c(1.2, 3.4); cout << c << endl; //输出 1.2 double n = 2 + c; //等价于 double n = 2 + c. operator double() cout << n << endl; //输出 3.2 c("八嘎呀路"); //函数对象的调用优先于普通函数且不产生重载 一个 // Complex z("世界"); //不能通过实例化调用 no matching function for call to 'Complex::Complex // void (*fun)(int) = c; //一个同名对象的产生 那个对应的同名函数将被编译器忽略作废 // fun(5); Complex *obj; cout << (*obj)("指针是怎么调用函数对象的函数的?") << endl; }
- 函数对象有个好处就是可以作为参数传递给其他函数,这种函数被称为高阶函数
>.在c++中不允许直接传递函数,虽然可以通过函数指针或引用的形式来传递,但与函数对象相比会产生间接调用的开销
二十一、Namespace
- 为了解决合作开发时的命名冲突问题,C++ 引入了名称空间(Namespace)的概念
-
#include <iostream> #include <vector> using namespace std; namespace Li{ //小李的变量定义 int a = 1 ; int b = 1; } namespace Han{ //小韩的变量定义 double a = 3.4; double b = 3.4; } int main(void) { //-------------------------普通用法------------------------------------ Li::a = 2; //使用小李定义的变量a Han::a = 1.2; //使用小韩定义的变量 a //-------------------------单个变量领域展开------------------------------------ //向下覆盖 领域展开 能覆盖掉无视掉 using namespace Han 且同一作用域内对此同名变量领域展开只能有一个人 using Li::a; // 默认后面所有的a都是指Li里面的a 除非用域解析符::指定 a = 3 ; //使用小李定义的变量 a using Han::a // error 且同一作用域内对此同名变量领域展开只能有一个人 using Han::b // ok Han::a = 4.4; //使用小韩定义的变量 a //-------------------------namespace领域展开------------------------------------ //同一作用域内只能有一个人namespace领域展开 using namespace Li; //向下覆盖 直到单个变量领域展开出现 或使用域解析符指定,否则都是用的Li里面的变量 a = 5; //使用小李定义的变量 a using Han::a ; //Han单个变量领域展开 a = 6.3 ; //使用小韩定义的变量 a cout<<Li::a<<' '<<Han::a<<endl; return 0; }
- 嵌套与扩展
-
扩展名称空间
namespace Li{ //小李的变量定义 int a = 1 ; int b = 1; } namespace Han{ //小韩的变量定义 double a=3.4; double b=3.4; } namespace Li{ //小李名称空间的继续, 被称为扩展名称空间的定义 char c= '0'; }
-
嵌套
namespace outer { double max(const std::vector<double>& data) { //body code.... } double min(const std::vector<double>& data) { //body code... } namespace inner { void normalize(std::vector<double>& data) { //... double minValue { min(data) };//Calls min() in outer namespace //... } } }
-
二十二、inline内联函数
- 函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中…
- 如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上
- 于是对于短小的函数 C++提供一种提高效率的方法, 即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function)
- 基本使用语法:
>.只需要在定义处使用inline关键字
inline void swap1(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
} - 使用inline修饰函数 只是告诉编译器你的意愿, 具体要不要这么做,编译器有自己的判断
>.如果函数体内代码复杂庞大,你还用inline修饰此函数 编译器大概是不鸟你的
- 和宏一样,内联函数可以定义在头文件中(不用加
static
关键字),并且头文件被多次#include
后也不会引发重复定义错误。 - 不能将内联函数的声明和定义分散到不同的文件中(没有意义)
- 类体内定义的函数自动成为内联函数
二十三、函数的可变参数
-
#include <iostream> #include <cstdarg> //可变参数的函数 void vair_fun(int count, ...) { va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { int arg = va_arg(args, int); std::cout << arg << " "; } va_end(args); } int main() { //可变参数有 4 个,分别为 10、20、30、40 vair_fun(4, 10, 20, 30,40); return 0; }
- 借助 va_arg 获取参数包中的参数时,va_arg 不具备自行终止的能力,所以程序中借助 count 参数控制 va_arg 的执行次数,继而将所有的参数读取出来。控制 va_arg 执行次数还有其他方法,比如读取到指定数据时终止。
- 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数
当可变参数中包含 char 类型的参数时,va_arg 宏要以 int 类型的方式读取;当可变参数中包含 short 类型的参数时,va_arg 宏要以 double 类型的方式读取。
To be continue…
总结
Better late than never