文章目录
- 应用领域
- C++对C的加强
- string类型介绍
- vector类型介绍
- 迭代器
- 类型转换
- 结构
- 类
- 模板与泛型
应用领域
项目如果既要求效率又要建模和高度抽象,选择C++;
系统层软件开发;
服务器程序开发;
游戏、网络、分布式、云计算;
科学计算
C++对C的加强
1.命名空间
2.对全局变量定义管理的加强(不能重定义),可以随时定义
(vs:F9加入断点,F5调试到断点)
int abc = {5}; //等号可有可无
int a[]{11,12,34};//定义数组
3对struct的增强
4.变量和函数必须有类型
5.布尔类型
6. 三目运算
在C++中三目运算符既可以当做值也可以当做变量
void main()
{
int a = 10;
int b = 20;
int c = 0;
% 正确
c = a < b ? a ; b % 如果a < b,则 c = a
*(a < b ? &a ; &b) = 50
% C++正确
(a < b ? a ; b) = 50
}
7.const常量
8.auto 变量的自动类型推断
在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,不会造成程序效率降低。
auto bvalue = true; //bool
9.头文件防卫式声明
在include时如果头文件中互相调用使得某个全局变量会发生重定义事件;
解决方法:在头文件head1里加
条件定义:保证中间的程序段只会被调用一次
#ifndef __HEAD1__H__ //如果没有定义这个头文件
#define __HEAD1__H__ //则定义这个头文件
//程序段
#endif
9.引用:为变量起了另外一个名字,一般用&表示;起完别名后,可以看成是同一个变量
引用不会额外占用内存;定义引用时必须初始化(即必须只能声明原变量)
int value = 10;
int &refval = value; //不是求地址运算符,只是起引用标识作用
refval = 3; //此时value也变成3
//注意区分
int *p = &value //指针时符号在等号右边
应用:函数调用
void func(int &a, int &b) //形参是引用类型,等于int &a=ta; int &b = tb;
{
a = 4;
b = 5;//这里改变值会影响到外界,和指针一样的效果
}
10.常量
const int var = 7;//承若这个变量的值不会被改变,不能再赋值
constexpr int var = 1; //关键字,可以修饰变量也可以修饰函数,
11.范围for语句
int v[]{12,13,14,15,16};
for (auto x:v) //遍历了数组的所有元素(将v中的元素依次拷贝到x中)
{
cout << x << endl;
}
for (auto &x:v) //省去了拷贝的动作,提高了效率(较高级的表达)
{
cout << x << endl;
}
//甚至
for (auto x:{12,13,14,15,16})
{
cout << x <<endl;
}
12.动态内存分配问题(重要)
c中供程序使用的储存空间,有程序区、静态储存区(全局变量)、动态储存区(局部变量)
c++中五个区域:
一.栈:存储一般函数的局部变量,由编译器自动存储和释放;
二.堆:程序员malloc/new分配,用free/delete释放;
三.全局/静态存储区:全局变量和静态变量
四.常量存储区
五.程序代码区
堆栈的区别:
一.栈空间有限;
二.堆:只要不超出实际物理内存,都可以手动分配,非常灵活,分配速度稍慢。
c中malloc的使用
char *point = NULL;
point = (char *) malloc(100* sizeof(char));
if (point != NULL)
{
strcpy(point, "hello world");
cout << point << endl;
free(point);
}
c++中new的使用,这里new是运算符而不再是函数,取代malloc;
本质的区别是new还会调用构造函数和析构函数,做了更多的初始化和释放工作;
使用格式一:指针变量名 = new 类型标识符;
使用格式二:指针类型名 = new 类型标识符(初始值);
使用格式三:指针类型名 = new 类型标识符[内存单元个数]
int *myint = new int;
if (myint != NULL)
{
*mayint = 8;
cout << *myint << endl;
delete myint;
}
int *myint = new int(18);
if (myint != NULL)
{
cout << *myint << endl;
delete myint;
}
int *pa = new int[100];//开辟大小为100的整形数组空间
if (pa != NULL)
{
int *q = pa; //q为pa的首指针
*q++ = 12;//[0] = 12
*q++ = 18;//[1] = 18,此时q已经指向了[2]
cout << *pa << endl; //12
cout << *(pa+1) << endl;//18
delete[] pa;//注意释放的格式,new用了[],回收也必须使用[]进行释放
}
##13.nulptr
代表空指针,即NULL,目的是防止NULL中整形和指针之间发生混淆
char *p = NULL;
p = nulptr;//true
char *q = nulptr;
int *pa = nulptr;
int a = NULL;
int a = nulptr; //错误,只能是对指针
14.函数与后置返回类型
函数定义中,形参如果在函数体内用不到的话,则可以不给形参变量名字,只给其类型;
函数声明时,可只有形参类型,没有形参名;
把函数返回类型放到函数名字之前,叫前置返回类型;
在函数声明和定义中,返回类型放在参数列表之后,成为后置返回类型:前面有auto,后面使用->放后置返回类型
auto func123(int a)->void
{
}
15.内联函数
在函数定义前增加了关键字inline,这个普通函数就成了内联函数;
对于函数体很小,调用很频繁的函数,通常定义为内联函数:在编译阶段时对这种函数进行处理,系统会将调用该函数的动作替换为函数本体,来提升性能。这种方法的好处是:调用函数会使用堆栈耗用资源,但是直接使用函数本体更加快捷。但inline是对编译器的一个建议,是否进行替换的决定权在编译器。
内联函数的定义放在头文件中,不会发生普通函数重定义的问题。
优缺点:
时间缩短但是代码膨胀,所以内联函数尽量小。
inline int myfunc(int testv)
{
return 1;
}
constexpr是常量调用的函数,像更严格的内联函数;
#define宏展开也类似于inline;
16.函数杂合用法总结
一.函数返回类型为void,表示函数没有返回类型,但是可以返回另一个返回值为void的函数;
二.函数返回 指针和返回引用的情况,比如返回一个局部变量的地址,当函数执行完毕后地址被系统回收,已经不能再使用;
三.没有形参可以保持形参列表为空,或者使用void
四.如果一个函数不调用,可以只有声明部分,而没有定义部分;
五.普通函数,定义只能定义一次,声明可以多次;
六.在c++中,更习惯用引用类型取代指针类型形参,因为这样可以省去拷贝的过程,效率更高;
七.在函数形参列表有所不同的情况下,函数名可以相同;
八. const char * \ char const * \ char * const的区别
const char *p //指针p指向的内容不能通过p来修改(可以通过其他方式进行改变,可以p++)
char const *p //与上面是等价
char * const p = str; //定义的时候必须初始化,p一旦指向一个内容/地址,就不能再指向其他内容(能通过*p对指向的内容进行修改,但不能进行p++这种操作,但是上面的可以)
const int &a = i; //引用:代表a的内容不能再修改,而i能修改,虽然a是i的别名
const int &b = 31; //如果不加const是错误的,不能引用常量,加了const之后b就代表了31
//上述所有情况对于函数形参调用时也使用
const还可以在函数的形参中加:(能加const和引用的都最好加上)
1.为了防止无意中通过函数的引用形参改变了实参的值;
2.实参类型可以更灵活:可以接收普通引用,还可以接收常量对象引用;
string类型介绍
string是c++标准库里的数据类型,是可变长字符串的处理。vector一种集合或者容器的概念。
使用前要加 #include <string>
string s1; //默认初始化,代表空字符串
string s2 = "China";
string s3("China"); //与上面的一样
string s4 = s2;
string s5(num, 'a');//num个a组成一个字符串(不推荐)
s1.empty(); //布尔类型
s1.size(); //字节/字符的长度(一个汉字两个字节,一个英文字符一个字节)
s1.length(); //和字节的长度是一样的
s1[n] // 支持索引,从0到n-1,并且支持更改
string s3 = s1 + s2 //字符串连接,得到新的string对象
s1 = s2; // 字符串内容传递
s1 == s2; //字符串相等,大小写敏感
s1.c_str(); // 返回一个指向c字符串的指针常量,也就是以/0结尾的
string s1;
cin >> s1; //此时输入的空格被忽略
cout << s1 << endl;
string s3 = "a" + "b"; //语法错误,不能将两个字符串直接相加,中间必须有string对象才行
string s3 = "a" + s1 + "b";// 语法正确
//范围for使用
for (suto c:s1) //自动推断类型
{
cout << c << endl;
}
for (suto &c:s1) //引用
{
c = toupper(c); //变大写
cout << c << endl;
}
vector类型介绍
一个标准库类型,代表集合或者动态数组的概念,可以放若干的对象,也被成为容器;
使用: #include <vector>
vector<int> vjihe; //表明此集合存放的是int型的数据/对象,<int>模板是类模板实例化的过程,vector是类模板
vector<vector<string>> str; //该集合里面每一个元素都是一个字符串的集合
vector<int *> vjihe; // <>里除了引用的类型其他基本都能装,因为引用是个别名而不是对象
//空vector初始化
vector <string> mystr; //创建了一个string类型的空vector对象(容器),目前没有任何元素
mystr.push_back("ab"); //加了一个元素,size = 1
mystr.push_back("asd"); //加了一个元素,size = 2
vector<string> mystr2(mystr); // 把mystr的元素拷贝到了mystr2中
vector<string> mystr3 = mystr; // 把mystr的元素拷贝到了mystr3中,三个容器的内容相同,但是地址不同
vector<string> mystr4 = {'a','b','c'}; // 使用列表初始化给容器值,采用类似集合的定义方式
//创建指定数量的元素(一般有元素数量概念有关的东西,一般用圆括号)
vector<int> ijihe(15,-20);//不给-20会默认0
vector<int> ijihe{15,-20};//两个元素,分别为15和-20
vector<int> i1(15);//表示是15个元素,现在元素都是 0
vector<int> i2{15};//表示有一个为15的元素
vector<string> sijhe(5,"hello");//不给初始默认空字符串
vector<string> s1{"hello"};//有一个为hello的元素
vecotr<string> s2{10};// 表示元素数量10,一般不建议这样写。建议初始化时{}里的类型与<>里的元素类型相同。
vector<int> ivec;
ivec.empty();//判断为空
ivec.push_back(1);//向末尾加入
ivec.size();//返回元素个数
ivec.clear();//清空所有元素
ivec[n];// 支持元素索引
ivec2 = ivec;//赋值(覆盖性)
ivec2 = {12,13};//赋值(覆盖性)
for (auto &ivecitem : ivec) //引用的范围for
{//注意在for语句中一定不要改变容器的容量,否则遍历过程会出错
}
迭代器
迭代器是一种遍历容器内元素的数据类型,是类似指针的数据类型,可以理解为迭代器用来指向容器内的某个元素。在c++中很少使用元素索引,而更过的是使用迭代器的方式访问容器中的元素。通过迭代器可以读容器中的元素值并修改迭代器指向的元素值。包含++、–等指针含有的操作。尽量使用迭代器来访问容器内的指针。
vector<int> iv = {1,2,3};
vecotr<int>::iterator iter;//每个容器都有一个iterator的成员,前面可以整个理解为一种数据类型,这种类型专门用于迭代器。当用这个类型定义一个变量的时候,这个变量就是一个迭代器,这里iter变量就是这个迭代器。
begin()/end()操作用来返回迭代类型
iter = iv.begin();// 如果容器中有元素,则begin返回的迭代器,指向的是容器的第一个元素(相当于iter指向了iv[0])
iter = in.end();//end返回的迭代器指向的并不是末端元素,而是末端元素的后面不存在的元素
//如果容器为空,返回的迭代器是相同的
vecotr<int> iv2;
vector<int>::iterator iterbegin = iv2.begin();
vector<int>::iterator iterend = iv2.end();
if (iterbegin == iterend):
{
cout << "iv2为空"<< endl;
}
通用的使用迭代器访问容器元素方法
vector<int> iv = {1,2,3};
for (vector<int>::iterator iter = iv.begin(); iter != iv.end() ; iter++) //传统for循环
{
cout << iter* << endl; //指针前加*代表指向的元素
}
反向迭代器:大家想从后往前遍历一个容器,那么反向迭代器就比较方便
rend指向的是第一个元素之前的位置,rbegin指向的是最后一个元素
vector<int> iv = {1,2,3};
for (vector<int>::reverse_iterator riter = iv.rbegin(); riter != iv.rend() ; riter++) //传统for循环
{
cout << *riter << endl; //指针前加*代表指向的元素
}
迭代器运算符
*iter返回迭代器iter所指向元素的引用,必须要保证这个迭代器指向的是存在的元素。end()指向的就是一个不存在的元素。
*iter++;++*iter让迭代器指向容器中的下一个元素,已经指向end后不能再++;
*iter–;--*iter让迭代器指向容器中的上一个元素,指向头之后就不能再进行–;
如果两个迭代器指向的是同一个元素则相等,否则则不相等;
引用结构体中的成员:
vector<student> sv;
student mystu;
mystu.num = 100;
sv.push_back(mystu);
vector<student>::iterator iter;
iter = sv.begin();
cout << (*iter).num << endl;
cout << iter->num << emdl;
const_iterator迭代器:迭代器指向的元素值不能改变,迭代器本身是可以改变的
const vector<int> iv = {1,2,3};//如果这里定义的是常量容器,则迭代器一定要使用常量迭代器;如果定义的是普通容器,则迭代器都可以,但是定义成常量迭代器之后元素值不能再改变
vector<int>::const_iterator iter;//常量迭代器
for (iter = iv.begin() ; iter != iv.end() ; ++iter)
{
cout << *iter << endl; //此时不能修改元素值,否则会报错
}
cbegin()和cend()返回的都是常量迭代器
for (auto iter = iv.cbegin() ; iter != iv.cend ; iter++)
{
cout << *iter << endl; //此时不能修改元素值,否则会报错
}
迭代器失效
vector<int> vec{1,2,3};
for(auto vecitem ; vec)
{
//for循环中不能对容器的元素个数进行修改。比如push_back。
}
所以需要进行删除和插入操作时,在操作之后立即break出循环,然后进行新的循环。
用迭代器遍历string
string str("China");
for (auto iter = str.begin() ; iter ! str.end() ; ++iter )
{
*iter = toupper(*iter);
}
cout << str << endl;
vector容器的常用操作和内存释放
struct conf{
char itemname[40];
char itemcontent[100];
}
char *getinfo*(vector<conf *> &conflist , const char *pitem)//根据输入的itername输出返回对应的itemcontent
{
for(suto pos = conflist.begin() ; pos != conflist.end() ; ++pos) //遍历conflist
{
if(_stricmp((*pos)->itemname, pitem) == 0)
{
return (*pos)->itemcontent;
}
}
return nullptr;
}
conf *p1 = new conf; //定义了结构体的指针
strcpy_s(p1->itemname, sizeof(p1->itemname), "severname");
strcpy_s(p1->itemcontent, sizeof(p1->itemcontent), "1区");
conf *p2 = new conf;
strcpy_s(p2->itemname, sizeof(p2->itemname), "severid");
strcpy_s(p2->itemcontent, sizeof(p2->itemcontent), "100");
vector<conf *> conlist;//这里使用了指针
conflist.push_back(p1);//容器的第一个元素为指向结构体的第一个对象的指针,注意不是结构体的变量
conflist.push_back(p2);
char *p_temp = getInfo(conflist, "seveername");
if (p_temp != nullptr)
{
cout << p_temp << endl;
}
//释放内存
std::vector<conf *>::iterator pos;
for (pos = conflist.begin() ; pos != conflist.end() ; ++pos) //遍历
{
delete (*pos)//这里删除指向的内存,因为指向的内容才是new的内容;而不是指针本身*pos,迭代器不能被破坏
}
//此时只有指针,没有指针指向的内容了
//conflist.clear()//删除指针可有可无
类型转换
隐式类型转换
系统自动进行,不需要人员介入。
int m = 3 + 45.6;
double n = 3 + 45.6;
显示类型转换(强制类型转换)
c语言
int k = 5 % 3.2; //错误
int k = 5 % (int)3.2; // 正确
int k = 5 % int(3.2); //正确
c++
4中强制类型转换
通用形式:强制类型转换名<type>express():
static_cast(静态转换)(正常转换)
可用于
a)相关类型转换
double f = 100.34f;
int i = static_cast<int>(f);
b)自类转换成父类
class A{};
class B : public A{};
A a;
B b = static_cast<B>(a);
c) void*与其他类型指针之间的转换, void *:无类型指针:可以指向任何类型指针(万能指针);
int i = 10;
int *p = &o;
void *q = static_cast<void *>(p);
int *db = static_cast<int *>(q);
不可用于: 一般不能用于指针类型之间的转换比如int *转double *, float * 转 double *等。
dynamic_cast
主要应用于运行时(其他都是编译时进行的)类型识别和检查。主要用来父类型和子类型之间转换用的。
const_cast
去除指针或者引用的const属性;
去除属性后仍然不建议修改值;
const int i = 90;
int i2 = const_cast<int>(i); //错误,因为不是指针或者引用
const int *pi = &i;
int *pi2 = const_cast<int *>(pi);
reinterpret_cast
编译时就会进行类型检查:将操作数内容解释为另一种不同的类型;
处理无关类型的转换:
a) 将一个整型地址转换成指针,一种类型指针转换成另一种类型指针,按照转换后的内容重新解释内存中的内容
b) 也可以从一个指针类型转换成一个整型。
总结:
1.强制类型转换,不建议使用,只能抑制编译器报错;
2.方便进行阅读;
3.最后一种危险,第三种意味着设计缺陷
4.建议使用c++风格的类型转换
5.第一种和最后一种可以很好取代c语言的类型转换
结构
当结构体做函数的形参的时候,有三种方式:就结构变量、变量的引用、变量的指针;建议使用后面两种;
自定义的数据类型
在c中:
struct student
{
int number;
char name[100];
}
void func(student stn)
{
stn.number = 2000;
strcpy_s(stn.name, sizeof(stn.name), "liai")
return 0;
}
void func1(student &stn)
{
stn.number = 2000;
strcpy_s(stn.name, sizeof(stn.name), "liai")
return 0;
}
void func2(student *pstn) //用指向结构体的指针做了形参
{
pstn->number = 2000;//注意使用指针后这里得到格式发生了变化
strcpy_s(pstn.name, sizeof(pstn->name), "liai")
return 0;
}
int main()
{
student st1;
st1.number = 100;
strcpy_s(st1.name, sizeof(st1.name)), "zhangsan"
cout << st1.number << endl;
cout << st1.name << endl;
func(sudent1);//结构体当参数调用函数
cout << st1.number << endl;//发现结果没变,说明函数并没有改变值。这是因为在调用函数的时候,将实参传递给形参是发生了内容的拷贝,效率很低。
cout << st1.name << endl;
func1(sudent1);//如果变成了引用,那就不会发生拷贝的过程,形参可以直接代表了实参
cout << st1.number << endl;//结果发生了改变
cout << st1.name << endl;
func1(&sudent1); //将指针作为形参,效率也较高,此时调用函数的时候稍微改变
cout << st1.number << endl;//结果发生了改变
cout << st1.name << endl;
}
c++的结构不尽有成员函数,还可以定义方法;
struct student
{
int number;
char name[100];
void func()
{
number ++;
return;
}
}
int main()
{
student st1;
st1.func();//直接调用成员函数
cout << st1.number << endl;//因为调用函数的影响,这里结果加1
cout << st1.name << endl;
}
类
自定义的数据类型;
注:类是c++的核心,尽量使用使用这种结构;
书写规范
一般将专门将类放到一个.h头文件中,将具体的实现放在一个.cpp文件中。(这是为了防止函数重定义,内联函数是例外可以直接定义在头文件中)
结构和类比较
一.类在c++中才有的;
二.struct和class定义的分别叫做,‘’结构变量‘’和‘’对象‘’。两者优势一块能够存储数据并且调用的储存空间;
三.在c++中类和struct非常相似,struct也属于一个类,但是默认是public,但是类默认private,尽量进行明确的指定;
public、private和protected(三种访问权限)
在类中public下的成员变量/成员函数,在类外部也能使用;(各种方法)
但在private下面定义的只能在类的内部进行访问;(一般放成员函数)
在外部使用类时,先定义一个对象,然后对象.方法就可以调用;
struct也属于一个类,但是默认是public,但是类默认private;
public:允许任意实体访问
protected:只允许本类或者子类的成员函数来访问
private:只允许本类的成员函数访问
面向对象和面向过程
面向过程为重视执行的过程;(更符合人的思维)
面向对象重视变量的变化。
面向对象求圆的周长和面积
#include <iostream>
using namespace std;
class Circle
{
public:
void setR(double r)
{
m_r = r;//这里不需要返回值,函数类型是void
}
double getR()
{
return m_r;
}
double getCirth()
{
return 2 * 3.14*m_r;
}
double getArea()
{
return m_r*m_r*3.14;
}
private:
double m_r; //私有成员变量
};
int main(void)
{
double r = 10;
Circle c; //用类定义对象
c.setR(r); //调用类的方法
cout << c.getR() << endl;
cout << c.getCirth() << endl;
cout << c.getArea() << endl;
getchar();
return 0;
}
类的多文件
新建项-类-circle.h,写明类的各种方法和变量的声明,但是不写具体的实现;
然后再circle.cpp中加
#include <circle.h>
并使用double Circle::setR()的格式来定义方法;
最后再主文件中加
#include <circle.h>,并可以对类进行调用,同时对三个文件进行编译
判断两个立方体是否体积相同
#include <iostream>
using namespace std;
class lifangti
{
public:
void setABC(double a, double b, double c)
{
m_a = a;
m_b = b;
m_c = c;
}
double getABC()
{
return m_a, m_b, m_c;
}
double V()
{
return m_a*m_b*m_c;
}
private:
double m_a ;
double m_b ;
double m_c ;
};
bool judge(double v1, double v2)
{
if (v1 == v2)
{
return 1;
}
return 0;
}
int main(void)
{
double a1;
double b1;
double c1;
double a2;
double b2;
double c2;
cin >> a1 ;
cin >> b1 ;
cin >> c1 ;
cin >> a2 ;
cin >> b2 ;
cin >> c2 ;
lifangti v1;
v1.setABC(a1, b1, c1);
cout << "ABC = " << v1.getABC() << endl;
cout << "v1 = " << v1.V() << endl;
lifangti v2;
v2.setABC(a2, b2, c2);
cout << "ABC = " << v2.getABC() << endl;
cout << "v2 = " << v2.V() << endl;
cout << judge(v1.V(), v2.V()) << endl;
system("pause");
return 0;
}
判断点是否在圆内
#include <iostream>
using namespace std;
class Circle
{
public:
void setXYR(double x, double y, double r)
{
x0 = x;
y0 = y;
r0 = r;
}
double getX()
{
return x0;
}
double getY()
{
return y0;
}
double getR()
{
return r0;
}
private:
double x0;
double y0;
double r0;
};
class Point
{
public:
void setXY(double x, double y)
{
x1 = x;
y1 = y;
}
double getX()
{
return x1;
}
double getY()
{
return y1;
}
private:
double x1;
double y1;
};
//判断点是否在圆内
bool judge(Point &p, Circle &c)
{
if ((sqrt((p.getX()-c.getX())*(p.getX() - c.getX()) + (p.getY()-c.getY())*(p.getY() - c.getY()))) < c.getR())
{
return true;
}
else
{
return false;
}
}
int main(void)
{
Circle c;
c.setXYR(2, 2, 2);
Point p;
p.setXY(1, 1);
if (judge(p, c) == true)
{
cout << "圆内" << endl;
}
else
{
cout << "圆外" << endl;
}
system("pause");
return 0;
}
析构和构造函数
构造函数
构造函数:在类中,有一种特殊的成员安徽省农户,它的名字和类名相同,我们在创建类的对象的时候,这个特殊的成员函数就会自动被系统自动调用。这个成员函数,就叫做构造函数。构造函数是在对象被创建的时候,用来初始化对象的函数。
在类中定义一个函数名与类名相同的函数,在声明类的对象时利用此构造函数可以直接进行初始化,调用函数时也不需要参数,没有返回值,函数之前也没有类型。
不可以手动调用构造函数,否则会出错。
正常情况下,构造函数应该被声明为public,因为类缺省的是私有成员,构造函数设为私有会报错。
构造函数中如果有多个参数,在创建对象的时候也要带上这些参数。
定义了有三个参数的构造函数后,创建新的类的对象时不同的创建方式:
Time myTime = Time(12,13,52);
Time myTime2(12,13,52);
Time myTime3 = Time{12,13,52};
Time mytime4{12,13,52};
Time myTime5 = {12,13,52};
多个构造函数
多个构造函数可以为类对象的创建提供多种初始化方法;
规定:
1.默认值只能放在函数声明里面,除非该函数没有函数声明;
2.在具有多个参数的函数中指定默认值时,默认参数必须出现在不默认参数的右边,一旦某个参数开始指定默认值,它右边的参数必须全部指定默认值。
隐式类型转换和explicit
在构造函数之前加explicit,防止进行隐式类型转换。
一般单参数的构造函数,都将其声明为explicit
构造函数初始化列表
以冒号开头,在构造函数里的大括号之前执行,格式为: :成员变量(形参)
析构函数
析构函数也没有形参没有返回值,在于类名相同的函数名之前加一个~代表析构函数,在一个对象在销毁之前,会默认调用析构函数,处理一些没用的与对象有关的内存;
系统会提供一个默认的无参构造函数和无参析构函数,提供了就会运行提供的函数。
inline内联
类内的成员函数实现其实也叫类内的成员函数定义。
这种直接在类内定义中实现的成员函数,会被系统当做inline内联函数来处理。(一般来讲类的定义和类内成员函数的实现定义是分开的,如果在一起则趋向于被认为是内联函数)
成员函数末尾的const
在成员函数第一行的最后增加一个const
不但在成员函数声明中增加const,也要在成员函数定义中增加const
作用:告知系统这个函数体的任何成员变量的值不能被修改。
类的const对象不能调用非const成员函数。
const成员函数,不管是const对象还是非const对象,都可以进行调用;然而非const成员函数,只能被非const对象调用,不能被const对象调用。
普通的函数末尾不能加const,类内专用。
mutable
不稳定,const的反义词。
用mutable修饰成员变量,一个成员变量一旦被mutable修饰了,成员变量的值就可以更改。
返回自身对象的引用-this指针
把自身对象返回;
在定义成员函数的实现的时候前面加 类名&
并在函数主题最后加 return *this (就代表这个对象)
this:在调用成员函数时,编译器负责把这个对象的地址传递给隐藏的this形参。在系统的角度,任何对类的成员调用都是通过this指针。this只在成员函数中才有,全局函数、静态函数都不能使用this指针。this这个const指针始终指向对象本身。
static静态成员变量
1.static成员变量实现了同类对象间信息的共享,不属于某个对象,而是属于整个类。对于这种成员变量的引用,用法为
类名::成员变量名
2.类外存储在静态区,求类的大小并不包括在内
3.只能类外初始化,因为初始化只进行一次;
4.可以通过类名访问、也可以通过对象访问
5.在成员函数之前也可以加static构成静态成员函数,属于真个类的成员函数。
static int a; //在类中声明静态成员变量,并没有分配内存和初始化
static void mstafunc(int testvalue); //声明静态成员函数
命名静态变量后,在属于类的静态区单独开辟空间,在类的public定义后只能写在类的外面进行调用, 如果在private定义后需要在public中写一个静态调用函数。在类外调用的时候可以不声明类的对象直接调用 类名::静态变量名。
一般在一个.cpp源文件的开头定义静态成员变量:
int Time::mystatic = 15; //可以不给初值,默认为0;在定义的时候可以不写static
cout << Time::mystatic <<endl; //可以使用类名直接进行调用,而不通过对象
cout << mytime.mystatic << endl; //与上面调用的都是同一个地址
静态成员函数定义:
依旧不加static
void Time::mstafunc(int testvalue)
{
//注意:这里可以修改静态成员变量值,但是不能修改某个类对象的成员变量,成员变量和成员函数是配套使用的。
}
cout << Time::mstafunc() << endl;
静态函数只能在该文件中使用;
静态功能使用举例:
#include <iostream>
using namespace std;
class Student {
public:
Student(int id, double score) {
m_id = id;
m_score = score;
m_count++; //静态变量为类内共享,与每次赋值的形参不同
sum_score += score;
}
static int getCount() {
return m_count;
}
static double getAvg() {
return sum_score / m_count;
}
~Student() { //析构函数
m_count++;
sum_score -= m_score;
}
private:
int m_id;
double m_score;
static int m_count; //定义静态变量,不过这里在private使用的,类外使用要专门定义静态方法
static double sum_score;
};
int Student::m_count = 0; //注意静态变量的初始化一定要在类外进行 采用类名::变量的格式
double Student::sum_score = 0.0;
int main(void) {
Student s1(1, 80);
Student s2(2, 90);
Student s3(3, 100);
cout << Student::getCount() << endl;//调用静态方法的时候可以直接使用而不用声明对象
cout << Student::getAvg() << endl;
system("pause");
return 0;
}
关于仓库管理的例子,融合了链表、指针、类、引用、静态等知识,如果能完全看懂说明就掌握好了
#include <iostream>
using namespace std;
class Good
{
public:
Good() //构造无参函数
{
weight = 0;//初始化
next = NULL;
cout << "创建了一个重量为" << weight << "的货物" << endl;
}
Good(int w) //有参构造
{
weight = w;
next = NULL; //指针初始化(未使用)
total_weight += w;
cout << "创建了一个重量是" << weight << "的货物" << endl;
}
static int get_total_weight() //导出静态变量的静态方法
{
return total_weight;
}
~Good() //析构函数
{
cout << "删除了一箱重量是" << weight << "的货物" << endl;
total_weight -= weight;
}
Good *next;//定义指针
private:
int weight;//每个货物重量,属于类的每一个对象的
static int total_weight;//仓库的总重量,属于整个类的
};
int Good::total_weight = 0; //类外静态初始化
void buy(Good * &head, int w) //添加到链表中,接收参数为指针建立操作,可以使用二级指针或者使用引用
{
Good * new_good = new Good(w);//用new创建一个货物,重量w,new_good代表这个货物的指针
if (head == NULL)//如果头指针为空,则此指针为头指针
{
head = new_good;
}
else
{
new_good->next = head;// 如果不为空,从头插入的话,则让之前的头指针(第一个货物)成为new_good的下一个货物
head = new_good; //新的货物指针成为头指针
}
}
void sale(Good * &head ) //从链表中减少
{
if (head == NULL)
{
cout << "没货了" << endl; //如果本身就没有货物
return;
}
else//如果有货物了,则
{
Good *temp = head; //定义一个临时指针,代表现在的链表的最后一个货物
head = head->next;//指针后退
delete temp; //删除此指针
}
}
int main(void)
{
int choice = 0;
Good *head = NULL; //定义一级指针
int w;
do {
cout << "1 进货" << endl;
cout << "2 出货" << endl;
cout << "0 退出" << endl;
cin >> choice;
switch (choice) //根据输入选择模式
{
case 1:
cout << "请输入要创建的货物的重量" << endl;
cin >> w;
buy(head, w);
break;
case 2 :
sale(head);
break;
case 0:
break;
default:
break;
}
cout << "当前仓库总量是" <<Good::get_total_weight()<< endl;
} while (1);
return 0;
}
this指针
用来区分变量是属于哪个类的对象的,在调用对象方法的时候,编辑器会自动传入一个this指针,指向条用该成员函数方法的对象地址;
静态成员函数只能访问静态数据成员,原因:非静态成员函数:在调用this指针被当做参数传递。而静态成员函数属于类,而不属于对象,没有this指针。
类相关非成员函数
与类的内容有关,但是不放在类之内的普通函数
.cpp中函数的实现:
void WriteTime(Time &mytime)
{
std::cout << mytime,Hour << std::endl;
}
.h中函数的声明
void WritetIME(Time &mytime):
类内初始化
为类内的成员变量提供一个初始值,在创建对象的时候,这个初始值就用来初始化该成员变量。
写法:
private:
int a = 10;//类内初始值
int b = {0};//类内初始值
Time::Time() //无参构造桉树
{
a = 10;
b = 0;
}
Time::Time(): a(10), b(0) //推荐使用这种
const成员变量初始化
const int value; //定义一个常量,如果只给其常量的属性但是没有对其进行初始化,系统会报错(可以在构造函数初始化进行初始化)
默认构造函数
没有参数的构造函数,为默认构造参数;
不管是否主动定义默认构造函数,都能够调用默认构造函数。
在类中没有构造函数的情况下,编译器会自动定义一个无参默认构造函数,也称合成的默认构造函数。
一旦自己定义了一个构造参数,不管有几个参数,系统就不会生成合成的默认构造参数。
=default, =delete
=default适用于生成特殊的函数体,比如不带参数的默认构造函数。
Time() {}; //定义默认构造参数
Time1() = default; // 作用于上面相同,编译器自动生成函数体。
//函数实现
Time2::Time2 = default;
=delete禁止生成某些特殊函数
Time2() = delete; //禁止生成默认的构造函数
拷贝构造函数(没完全懂)
默认情况下,类对象之间的拷贝是成员变量逐个拷贝。
如果没有定义拷贝构造函数,系统会自动定义合成构造函数,这种合成拷贝构造函数会将成员变量逐个拷贝到正在创建的对象中。每个成员的类型决定了如何拷贝,如果成员变量是整型的则将值直接拷贝,如果是类类型则调用拷贝构造函数。
如果人为定义了拷贝构造函数,拷贝构造函数会取代系统合成拷贝构造函数,需要在自己定义的对类成员进行赋值,以防出现没有赋值就直接使用的情况。
如果一个构造函数,第一个参数是所述类类型的引用,如果还有额外的参数,而且参数都有默认值(默认参数都放在函数声明中)。则这个函数就叫拷贝构造函数。
拷贝构造函数的作用是在一定时机被系统自动调用:
时机就是对象复制拷贝
Time mytime2 = mytime;
Time mytime3(mytime);
Time mytime4{mytime};
Time mytime5 = {mytime}
定义:
Time(Time &mytime, int a = 56);
实现:
Time::Time(const Time &mytime, int a) //习惯在第一个参数之前加const,这样应用的范围更广
{
//
}
注:拷贝构造函数一般不声明为explicit禁止隐式转换;
还有一些调用拷贝构造函数的情况:
当把一个对象传递给一个非引用类型的形参,会发生拷贝构造函数的调用;
func(mytime); //会调用拷贝构造函数
在函数中临时定义一个类对象,并将这个临时对象返回时会调用拷贝构造函数;此时系统会产生一个临时对象,并将值赋给这个临时对象;即return的临时对象地址和函数对象地址是不一样的。
Time mytime;
return mytime;
重载运算符应用于类对象中
一般使用的重载运算符
==,>,>=,!=,++,–,-=,+=,cout,cin,<<,>>
在比较两个类对象时,我们需要写一个成员函数,里面包括了需要的比较逻辑。
重载运算符本质是一个函数,整个函数的正式名字:operator关键字 +运算符;
作为一个函数,也是有返回类型和参数列表的。
有一些运算符,系统会自动生成一个,比如赋值运算符的重载。
拷贝赋值运算符
Time mytime;//调用构造函数
Time mytime2 = mytime;
Time ytime5 = {mytime};//调用拷贝构造函数
Time mytime6;
mytime6 = mytime5;//赋值运算符,系统调用了拷贝赋值运算符
可以自己重载赋值运算符,如果我们不重载,编译器也会为我们生成一个;
为了精确控制类的赋值动作,往往自己来重载赋值运算符;
重载赋值运算符:有返回类型和参数列表,这里的参数表示运算符的云算对象。
定义部分:
Time & operator = (const Time&);//给mytime5常量属性以防修改
实现部分:
Time& Time::operator = (const Time& tmpobj) //mytime5作为参数传递进来
{
return *this;//返回的是mytime6的引用
}
析构函数
对象在销毁的时候会调用析构函数,如果不自己定义析构函数,系统为生成默认的析构函数,这个默认析构函数的内容为空;
析构函数也是类的成员函数,它的名字是由~+类名构成,一定没有返回值,不能被重载,所以给顶一个类,只有唯一一个析构函数;
函数重载:系统允许函数名字相同,但是参数个数或者参数类型不同,这种同名函数的存在;当调用这些函数的时候,系统会跟据参数判断属于哪一个函数。
对于析构函数来说不村在函数重载,因为析构函数是函数名唯一的。
构造函数中成员变量初始化:1)函数体之前使用初始化列表的方式进行成员变量初始化2)在函数体中进行赋值;
析构函数的成员销毁:1)析构函数的函数体中对认为new的空间进行delete,这里不销毁成员变量 2)函数体执行后系统进行类的成员变量销毁
成员变量初始化和销毁时机:初始化按照类中定义的顺序来进行初始化,销毁按照定义的逆序进行销毁(初始化和销毁的时机像入栈出栈一样)
new和delete对象
new的时候调用了该类的构造函数,但是注意要自己进行释放,系统默认的析构函数并不会进行释放,不释放会造成内存泄露,最终导致崩溃。
Time *pmytime5 = new Time;//用new新建对象指针,调用无参构造函数
Time *pmytime5 = new Time();//看做和上面相同
delete pmytime5;//主动进行释放
派生类
类之间有层次关系,如父类/基类/超类(车)、子类/派生类(卡车、轿车);
则称这两钟类之间的类的关系为继承,继承是面向对象程序设计的核心思想之一;
这种继承要先定义一个父类,这个父类中定义一些公用的成员函数或者成员变量,然后通过继承父类来构建新的子类,写代码时之畜药写和子类相关的内容即可(通常子类会比父类更加庞大,有更多的成员变量和函数)
格式:
class 子类名 : public/protected/private 父类名// 这一行表示men是由human派生的
基类定义:
//定义基类
# ifndef __HUMAN__
# define __HUMAN__
# include <iostream>
class Human
{
public:
Human();
Human(int);
public:
int m_age;//
char m_name;//
};//这里分号一定要加
# endif
基类实现
Human::Human()
{
//
}
Human::Human(int abc)
{
//
}
子类定义
# ifndef __HUMAN__
# define __HUMAN__
# include <iostream>
# include "human.h" //将父类的定义导入
class Men : public Human// 这一行表示men是由human派生的
{
publlic:
Men();
public:
//
}
子类实现
Men::Men
{
//
新建一个子类的对象时,会先调用父类构造函数,再调用子类的构造函数。函数体也是先执行父类的,再执行子类的。
继承方式问题:
protected能被本类和子类进行调用;
1)子类public继承不改变父类的访问权限;
2)protected继承将父类中public成员变为子类中的protected成员;
3)private继承使得父类所有成员在子类中变成private;
4)父类中的private子类永远无权访问;
基类中的权限 | 子类继承的方式 | 子类得到的访问权限 |
---|---|---|
public | public | public |
protected | public | protected |
private | public | 无权访问 |
public | protevted | protected |
protected | protected | protected |
private | protected | 无权访问 |
public | private | private |
protected | private | private |
private | private | 无权访问 |
函数遮蔽
当父类和子类中有完全同名的两个函数,父类的同名函数无法通过子类对象进行访问。如果一定要对父类中的同名函数进行调用,可以:
1)在子类的成员函数中,使用 父类::函数名 的格式对父类的成员函数进行调用;
2)通过using关键字,让父类的同名函数在子类中可见(实现在子类对象中调用父类的重载版本,子类和父类中的同名函数参数个数还是要不一样,不然这种方法还是无法调用到父类的函数)
//在子类中添加using关键字
public:
using Human::samenamefunc:
友元函数
类的友元函数可以访问此类的所有成员变量;
加友元函数的方法:在函数名之前加 friend,友元函数的声明不受public,private,protected的限制;
友元类
类可以把其他的类定义为友元类,友元类可以访问这个类的所有成员;
class A
{
friend class C;//C是A的友元类,C应该先定义
}
class C
{
}
注意:
1)友元关系不能拿=被继承;
2)友元关系是单向的,比如上边类C是类A的朋友,但是反过来不行;
3)友元关系是没有传递性的
友元成员函数
只有public的函数才能成为其他函数的友元函数;
在类A中声明类C的某函数为类A的友元成员函数,此函数可以访问A中的public、private成员:
friend void C::callCAF(int x, A &a);//声明
RTTI(不懂)
run_time_type_identufucation运行时类型识别
通过运行时类型识别,程序能够使用基类的指针或者引用 来检查这些指针或者引用所指的对象的实际派生类型。
//假设men和women是子类,human是基类
Human *phuman = new Men;//使用父类指针new一个子类对象
Human &q = *phuman;//*phuman表示指针phuman所指向的对象,这里引用q是别名
引入RTTI能够知道phuman指向是哪个子类对象;
RTTI可以看成是一种系统提供给我们的一种能力,或者一种功能。这种功能通过两个运算符实现:
1)dynamic_cast:能够将基类(Human)的指针(phuman)或者引用(q)安全的转换成派生类(Men)的指针或者引用;
指针转换格式:Men *pmen = dynamic_cast<Men * >(phuman);
引用转换格式:Men menbm = dynamic_cast<Men &>(q)
Human *phuman = new Men;
Men *p = (Men *)(phuman);//C语言风格强制将指针转换称Men*
p->testfunc();//发现能够正常调用Men类成员函数func()
Women *p = (Men *)(phuman);
p->testfunc();//此时会发生问题,因为不知道这个指针到底指向的是哪个子类
dynamic_cast如果该运算符能够转换成功,说名这个指针实际上是要转换到的那个类型
Human *phuman = new Men;
Men *pmen = dynamic_cast<Men * >(phuman);//c++风格,d_c后面加希望转到的子类和要转的指针或者引用
if (pmen != nullptr)//测试是否转换成功
{
cout << "phuman实际是一个Men类型" << endl;//此时Men里的成员操做都可以操作
}
else
{
cout << "不是一个Men类型" <<endl;
}
上面是指针,如果是引用的情况:用dynamic_cast转换失败后系统会抛出异常。用try和catch可以捕捉这个异常。
Human *phuman = new Men;
Human &q = *phuman;
try
{
Men menbm = dynamic_cast<Men &>(q);//转换不成功则程序进入到catch中,否则继续进行
cout << "phuman实际是一个Men类型 " << endl;
}
catch(std::bad_cast)
{
cout << "phuman实际不是一个Men类型 " << endl;
}
2)typeid:返回指针或者引用所指对象的实际类型;
能得到对象类型信息;typeid会返回一个常量对象的引用,这个常量对象是一个标准库类型 type_info(类/类类型)
Human *phuman = new Men;
Human &q = *phuman;
cout << typeid(*phuman).name() << endl;//认为typeid指向的是Men类类型
cout << typeid(q).name() << endl;
typeid主要是为了比较两个指针是否指向同一类型的对象;
1)两个指针定义的类型相同(Human),不管new的是什么,typeid都相等
Human *phuman = new Men;
Human *phuman = new Women;
if (typeid(phuman) == typeid(phuman2))
{
cout << "phuamn 和phuman2是同一种类型[看指针类型]" << endl;//
}
比较对象时,看的是new出来的是哪个对象或者该指针指向的是哪个对象,和定义该指针时定义的类型没关系
Human *phuman = new Men;
Men *phuman2 = new Men;
Human *phuman3 = phuman2;//这三个都属于一个类型的
if(typeid(*phuman) == typeid(*phuman))
{
cout << "phuman phuman2指向的对象类型相同" << endl;
}
if(typeid(*phuman2) == typeid(*phuman3))
{
cout << "phuman2 phuman3指向的对象类型相同" << endl;
}
注:要想让RITI的两个运算符能够正常工作,那么基类中必须至少有一个虚函数(多态类类型)。因为只有虚函数的存在,这两个运算符才会使用指针或者引用所绑定的对象的动态类型(new的类型)。
type_info类
typeid会返回一个常量对象的引用,这个常量对象是一个标准库类型type_info(类/类类型)
a).name名字 :返回一个c风格字符串
Human *phuman = new Men;
const type_info &tp = typeid(*phuman);
cout << tp.name() << endl;
b) == , !=
Human *phuman = new Men;
const type_info &tp2 = typeid(&phuman2);
if (tp == tp2)
{
cout << "tp 和tp2类型相同" << endl;
}
RTTI与虚函数表
c++中,如果类中有虚函数,编译器就会对该类产生一个虚函数表;
虚函数表里有狠多项,每一项都是一个指针,每个指针指向的是这个类里的各个虚函数的入口地址。虚函数表项里,第一个表项很特殊,它指向的不是虚函数的入口地址类所关联的type_info对象。
基类与派生类关系
派生类对象模型简述
Men mymen;//子类(派生类)对象,包含多个组成部分(也就是多个子对象)
一个是含有派生类自己定义的成员变量,成员函数的子对象;
一个是该派生类所继承的基类的子对象,这个子对象中包含的是基类中定义的成员变量、成员函数。
Human *phuamn = new Men;//基类指针可以new派生类对象,因为派生类对象含有基类部分,所以我们可以把派生类对象当成基类对象用。
所以可以用基类指针new派生类对象,编译器做了隐式这种派生类别的转换,好处就是有些需要基类引用的地方可以用这个派生类对象的引用来代替。如果有些需要基类指针的地方也可以用派生类对象的指针来代替。
派生类构造函数
Men mymen;
派生类实际是使用基类的构造函数来初始化它的基类部分,基类控制基类部分的成员初始化,派生类控制派生类部分成员的初始化。
传递参数给基类构造函数的问题:通过派生类的构造函数初始化列表;
class A //继承
{
public:
A(int i):m_value(i){};//构造函数和初始化列表
private:
int m_value;
}
class B:public A //不写继承方式,默认私有继承
{
public:
B(int i, int j, int k):A(i),m_valueB(k){};//构造函数和初始化列表,注意新加的 类名(参数名) ,通过子类的初始化列表给父类的构造函数传参
private:
int m_valueB;
}
定义派生类的对象后的调用顺序:
构造:基类的构造函数–派生类的构造函数。
析构:派生类的析构函数–基类的构造函数。
既当父类又当子类
继承关系一直传递,构成了一种继承链,最终结果就是派生类son会包含它的直接基类的成员以及每个间接基类的成员;
不想当基类的类
final加在类名后面
class A final;//第一种
class B final : public A;//第二种
静态类型与动态类型
静态类型:声明时候的类型,静态类型编译的时候是已知的
Human *phuman = new Men();//基类指针指向一个派生类对象
Human &q = *phuman;//基类引用绑定到派生类对象上
动态类型:指的是这个指针/引用所代表的(所表达的)内存中对象的类型,这里是Men类型。动态类型是在运行的时候才能知道。
如果不是基类的指针/引用,那么动态类型和静态类型永远都应该是一致的
派生类向基类的隐式类型转换
Human *phuman = new Men();//基类指针指向一个派生类对象
Human &q = *phuman;//基类引用绑定到派生对象上
编译器隐式的帮我们执行了派生类到基类的转换;
这种转换之所以能够成功,是因为每个派生类对象都包含一个基类对象部分,所以基类的引用或者指针是可以绑到基类对象这部分。
基类对象能独立存在,也能作为派生类对象的一部分存在。但并不存在从基类到派生类的自动类型转换。
父类子类的拷贝与赋值
Men men;//派生类对象
Human human(men);//可用派生类对象来定义并初始化基类对象,这个会导致基类的拷贝构造函数的执行
Men men;
Human human;
human = men;//拷贝
用派生类对象为一个基类对象初始化或者赋值时,只有该派生类对象的基类部分会被拷贝或者赋值,基类只干基类自己的事。
左值右值
左值:能当作赋值语句等号左侧的东西,可以看作是一个地址;
右值:不能当作左值的都是右值;
一般表达式,不是左值就是右值;
左值有的时候可以当右值使用;
i = i + 1;//i是个左值,不是右值,i在等号右边的时候,具有右值属性,出现在左边的时候有左值属性;
用到左值的运算符:
a)赋值运算符
a = 3;
b)取地址
&a;
c)string,vector下标[]都需要左值
abc[0];
vector::iterator iter;
iter++;
iter–;
d)通过看一个运算符在一个字面上能不能操作,我们就可以判断运算符是否用到的是左值;
左值:代表一个地址,所以左值表达式的求值结果,对应一个对象,就得有地址;
引用分类
a)左值引用
int value = 10;
int &refval = value;
refval = value;
b)const引用
const int %refval2 = value;
c)右值引用(绑定到右值)
int &&refrightvalue = 3;//绑定到一个常量
refrightvalue = 5;
左值引用
引用左值,绑定到左值上;
没有空引用的说法,所以左值引用初始化的时候就绑定左值;
int a =1;
int &b{a};//正确
int &c;//错误,没有初始化
int &c = 1;//错误,左值引用不能绑定到右值,必须绑定到左值
右值引用
必须是绑定到右值的引用;
能绑定到左值上的引用,一般不能绑定到右值;反过来也是一样;
int &&refrightvslue = 3;
string str{"abc"};
string &rl{str};//正确
string &r2{"abc"};//错误,左值引用不能绑定到临时变量
const string &r3{"abc"};//正确,创建个临时变量,绑定到左值r3上去
const引用不但可以绑定到右值,还可以执行到string的隐式类型转换并将所得到的值放到string临时变量中;
string &&r4{str};//错误,右值不能绑定到左值
string &&r5{"abc"};//正确
int i =10;
int &r1 = i;//正确,左值引用
int &&r2 = i;//错误,不能将右值绑定到左值上
int &&r3 = i*100;//可以,右值引用绑定到右值
总结:返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符,都是返回左值表达式的例子,可以将左值引用绑定到这类结果上;
返回非引用类型的函数,连同算数,关系以及后置递增递增运算符,都生成右值,不能将左值引用绑定到这类表达式上,但是可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
特别注意:
i++看成是右值,无地址的数字
++i看成是左值,有地址的变量
int i = 1;
int &&r1 = i++;//成功绑定右值,但是此后r1的值跟i没有关系,因为r1绑定的是i++返回的一个临时量而跟变量无关
int &r2 = i++;//错误,不能绑到右值上;
int &r3 = ++i;//r3绑到i,r3就变成i的别名了
int &&r4 = ++i;//错误,右值表达式不饿能绑定到左值表达式
右值引用的引入目的
a)&&可以代表一种新的数据类型;
b)可以提高程序运行效率,把拷贝对象变成移动对象来提高程序运行效率;
c)移动对象如何发生。&&(应付移动构造函数,应付移动赋值运算符用的)
std::move函数
加std是为了防止move函数名和其他混淆,这个函数的作用是将一个左值强制转换为一个右值,并没有移动的作用。带来的结果是右值可以绑定上去了;
int i = 10;
int &&ri20 = i;//错误
int &&ri20 = std::move()i;//将左值变为右值,可以绑定上去绑定上去ri20就代表i了
int &&ri6 = 100;//
int &&ri8 = ri6;//错误,左值不能绑定到右值上去
int &&ri8 = std::move(ri6);//将左值变为右值,可以绑定上去
string st = "abc";
string def = std::move(st);//将st中的内容拷贝到def中,st中的内容变空,调用了string里的移动构造函数,而不是std::move的移动作用
string &&def = std::move(st);//与上面不同,不会触发移动构造函数,左值转右值,此时st内容并没有发生变化
一般使用std::move后就不再使用该参数;
临时对象
一些临时对象是由代码书写问题产生的,统一称临时变量为临时对象。
产生临时对象的情况和解决:
a)以传值的方式给函数传递参数
定义和赋值最好在同一条语句完成;
直接传值会调用拷贝构造函数,所以不建议直接传值,建议引用传值
b)类型转换生成的临时对象/隐士类型转换以保证函数调用成功
在调用转换类型的构造函数上也出现了临时对象;
只会为const引用产生临时变量;
c)函数返回对象
在函数内新建对象;
临时对象是一个右值,可以绑定右值
尽量减少临时对象的产生;
对象移动
1)A移动B,那么对象A就不再使用了;
2)移动并不是把数据从一个地址移到另一个地址,只是把地址的所有者进行变更;
拷贝构造函数和移动构造函数的对比:
Time::Time(const Time &tmptime){...}//拷贝构造函数,const左值引用&
Time::Time(const Time &&tmptime);//移动构造函数,const右值引用&&
移动构造函数和移动赋值运算符应该完成的功能:
1)完成必要的内存移动,斩断原对象和内存的关系;
2)确保移动后原对象处于一种“即使销毁也没有问题”的状态;
B *pb = new B(); //调用B的构造函数
pb-> m_mb = 19;
B *pb2 = new B(*pb);//只有拷贝构造函数的时候调用类B的拷贝构造函数,有移动构造函数的时候调用移动构造函数
delete pb;//调用B析构函数
delete pb2;
在写移动构造函数的时候,注意要将之前的指针清空,因为比变成新的移动对象指针指向该地址了;
演示
A a = getA();//一个析构函数,一个移动构造函数,1个析构函数
A a1(a);//1个拷贝构造函数
A a2(std::move(a));//建立了新对象,调用新对象a2的移动构造函数
A &&a3(std::move(a));//没有建立新对象,没有调用移动构造函数,效果等同于对象a有新别名a3,后续建议使用a来操作
拷贝赋值运算符
A& operator = (const A& src)
{
if (this == &src)
return *this;
delete m_pb;
m_pb = new B(*(src.m_pb));
return *this;
}
移动赋值运算符
A &operator = (A && src) noexcept
{
if (this == &src)
return *this;
delete m_pb;//删掉自己的内存
m_pb = src.m_pb;//对方的内存直接指过来
src.m_pb = nullptr;//斩断源
return *this;
}
演示
A a = getA();//一个构造,一个移动构造,一个析构
A a2;//一个构造
a2 = std::move(a);//移动赋值运算符
某些条件下,编译器能合成移动构造函数,移动赋值运算符:
a)有自己的拷贝构造函数,自己的拷贝赋值运算符,或者自己的析构,那么编译器就不会为它合成移动构造函数和移动赋值运算符。
b)如果没有自己的移动构造函数和移动赋值运算符,可以调用自己的拷贝构造函数和拷贝赋值运算符来代替 ;
c)只有一个类没定义任何自己版本的拷贝构造成员,且类的每个非静态成员都可以移动时,编译器会合成移动构造函数,移动赋值运算符
什么叫成员可以移动?
a)内置类型可以移动
b)类类型,如果有对应的移动操作相关的函数时,就可以移动;
总结:
a)尽量给类增加移动构造函数和移动赋值运算符
b)不抛出异常的要加 noexcept
c)移动构造函数里注意要将原指针置空,让被移动对象随时处于一种能够被析构的状态;
d)没有移动,会调用拷贝代替
继承的构造函数
一个类只继承直接积基类的构造函数。默认、拷贝、移动构造函数是不能被继承的。
如果基类含多个构造函数,则多数情况下,派生类会继承所有这些构造函数,但如下例外情况:
a)如果过在派生类中定义的构造函数与基类构造函数有相同的参数列表,那么从基类中继承的构造函数会被在怕派生类中的覆盖,相当于只继承了一部分的构造函数;
b)默认、拷贝、移动构造函数不会被继承
多重继承(一般不建议)
从多个父类来产生出子类;
class C: public A, public B//多重继承
{
public:
C(int i, int j, int k): A(i), B(j), m_value(k)
{
}
virtual ~C()//虚析构函数
{
}
void myintfoC()
{
A::myinfo();//调用A的函数
B::myinfo();
myinfo();//默认是自己的函数
}
}
int main()
{
C ctest(10,20,50);
ctest.myinfoc();//先调用A和B的构造,再调用C的构造函数
ctest.A::myinfo();//调用类A中的某个函数,防止函数名相同的情况。如果自己的类中和父类中有同名函数,直接调用会遮蔽父类的函数,所以调用父类要特别强调!
}
多重继承中的静态变量:
静态成员属于类,不属于对象,所以用类名来引用静态变量,也可以用对象名引用静态变量
Grand::m_static =1;
A::m_static = 2;
ctest.m_static = 9;
派生类构造和析构函数
a)新建新的派生类对象,将同时构造并初始化所有的基类子对象;
b)派生类的构造函数初始化列表只初始化它的直接基类。每个类的构造函数都负责初始化它的直接基类,就会让所有类都得到初始化;(最先执行爷类,然后是父类)
c)派生类的构造函数初始化列表将实参分别传递给每个基类;基类的构造顺序跟派生类中基类的出现顺序保持一致
d)先构造的后析构;
从多个父类继承构造函数
如果一个类从基类中继承了相同的构造函数,这个类必须为该构造函数定义它自己的版本;
C(int tv):A(tv),B(tv){};//定义自己的版本
类型转换
基类指针可以指向一个派生类对象:编译器会隐士执行这种派生类到基类的转换,转换成功的原因是每个派生类对象都包含一个基类对象部分。
所以基类的指针或者引用可以绑到基类对象上这部分的。
虚派生
派生列表中,同一个基类只能出现一次,但是如下情况例外:
a)派生类可以通过它的两个基类分别继承同一个间接基类;
b)直接继承某个基类,然后通过另一个间接继承该类
这些情况导致有类被构造了两次,占空间,还可能产生名字冲突;
虚基类:无论这个类再继承体系中出现多少次,派生类中只会包含唯一的共享的虚基类子内容;
a)虚基类是由最底层派生类来进行初始化,而不是直接子类
b)初始化顺序问题:先初始化虚基类部分,然后再按照派生列表中出现的顺序来初始化其他基类
注:能不用多重继承就不用
继承的构造函数(听不懂)
类型转换构造函数(只带一个参数的构造函数,将其他类型转换为类类型)
构造函数种类:默认构造函数(不带参数)、拷贝构造函数、移动构造函数、带n个参数
构造函数特点回顾:
a)函数名与类名相同
b)没有返回值
有一种构造函数,叫类型转换构造函数,主要能力是:可以将某个其他的数据类型转换成该类类型的对象,其特点为:
a)只有一个参数,该参数又不是本类的const引用,该参数是带待换的数据类型,所以带转换的数据类型不应该是本类类型
b)在类型转换构造函数中,要指定转换的方法
TestInt ti = 12;//做了隐式类型转换,并调用了类型转换构造函数。编译器用12这个数字通过调用类的类型构造函数创建一个临时对象,并把这个对象,并把对象构造到了ti预留空间中去。
TestInt ti = TestInt(12);//没有进行隐式类型转换
TestInt ti2(12);//没有进行隐式类型转换,并调用了类型转换构造函数
类型转换运算符/类型转换函数
能力和类型转换构造函数相反,是类的一种特殊的成员函数,能够将一个类类型对象转换成其他类型的数据。
格式:
operator type() const;//
说明:
a)const是可选项,表示一般不应该改变待转换对象内容
b)type:表示要转换成的某种类型,只要是能够作为函数返回的类型,都可以
c)类型转换运算符,没有形参(空),因为类型转换运算符一般都是隐式执行的,所以没有办法传递参数,同时也不能指定返回类型,但是却能返回一个type指定的类型的值
d)必须定义为类的成员函数
TestInt ti2;
ti2 = 6;//编译器用6生成对象(调用类型转换构造函数),又调用赋值运算符把临时对象内容给了ti2
//两种使用方法
int k = ti2 + 5;//int 转ti2是隐式转换,隐式调用,调用operator int () const将ti2转换成了int,再和5做加法运算,结果给k。在函数之前加explicit防止做隐式类型转换,此条语句就会报错。
int k2 = ti2.operator int() + 5;//显式调用,没有形参,()为空
显式的类型转换运算符
范例:类对象转换为函数指针
类型转换的二义性问题
建立:在一个类中,尽量只出现一种类型转换运算符;
类成员函数指针
是个指针,指向类成员函数;
a)对于普通成员函数
格式 类名::*函数指针变量名
类名::成员函数名 来获取地址
void (CT ::*myfpointpt)(int);//一个类成员函数指针变量的定义,变量名字为myfpoint
myfpointpt = &CT::ptfunc;//类成员函数指针变量myfpointpt被赋值
成员函数是属于类的,不属于类对象,只要有类在就有成员函数地址在。
但是要使用这个成员函数指针,就必须要绑定到一个类对象上才能用;
使用函数指针的方式: “类对象名.*函数指针变量名”来调用,如果是个对象指针,则调用格式“指针名->函数指针变量名”来调用
对于虚函数
void (CT ::*myfpointvirtual)(int) = &CT::virtualfunc; //内存地址
也必须要绑定到类对象上才能调用
对于静态成员函数
使用“函数指针变量名”来声明静态成员函数指针,使用“&类名::成员函数名”来获取类成员函数地址。这个也是真正的地址。
定义一个静态的类成员函数指针并赋值
void(*myfpointstatic )(int) = &CT::staticfunc;
myfpointstatic(100);
类成员变量指针
对于普通成员变量
int CT::*mp = &CT::m_a;//定义一个成员变量指针,不是指向内存中某个地址,而是该成员变量与该类对象之间的偏移量
CT ctestmp;//d当生成类对象时,如果这个类中有虚函数表,则对象中会有一个指向这个虚函数的指针,这个指针占有四个字节。
对于静态成员变量
这种指向静态成员变量的指针,是由真正的内存地址的;
int *stcp = &CT::m_stca;//静态成员变量指针
*step = 796;
cout << *step << endl;
模板与泛型
1)泛型编程,是以独立于任何特定类型的方式编写代码。使用过泛型 编程时,需要提供聚体程序实例所操作的类习惯或者值;
2)模板时泛型变成的基础。模板是创建类或者函数的蓝图或者公式。给这些蓝图或者让公式提供足够的信息,让这些蓝图护着公式真正的转变具体的类或者函数,然后这种转变发生在编译时。
3)模板支持将类型作为参数的程序设计方式,从而实现了对泛型程序设计的直接支持,也就是说,模板机制支持将类型作为参数。
模板分为函数模板和类模板
函数模板
两个函数重载,结构一样只是类型不一样
int funcadd(int i1, int i2)
{
int addhe = i1 + i2;
return addhe;
}
double funcadd(double i1 , double i2)
{
double addhe = d1 + d2;
return addhe;
}
定义一个模板适合多个类型
template <typename T>
T funcadd(T a, T b)
{
T addhe = a+ b;
return addhe;
}
1)模板定义使用template关键字开头的,后面<>里面的叫模板参数列表(模板实参)。<>里至少要有一个模板参数,模板参数前面有个typename/class关键字,表示这是个类型名。如果有多个模板参数,就要用多个typename中间用逗号分隔。
2)模板参数列表里面表示在函数定义中用到的“类型”或“值”,也和函数参数列表类似。
用的时候,有时候得指定模板实参给他,指定的时候要用<>把模板实参包起来。有时候又要指定模板实参。
3)funcadd这个函数声明了一个名字为T的类型函数。这个T代表的类型系统会根据调用函数时候针对性确定。
函数模板调用和函数调用区别不大,调用的时候,编译器会根据调用这个函数模板时的实参去推断模板参数列表里的参数的类型。注意模板类型有时是推断出来的,有时候需要提供。有时光凭借函数实参是推断不出来的,这时需要<>来主动提供类型参数。
非类型模板参数:
typename代表T是一个类型,是一个类型参数。那么在模板参数列表里,还可以定义非类型参数,代表一个值。比如非类型参数如果是个整形,int s。
template <typename T, int S>
T funcadd(T a, T b)
template <int a, int b>
int funcadd2()
int result = funcadd2<12,13>();//显示的指定模板参数--在尖括号中提供额外信息。且这里不能用变量只能用常量。
当模板被实例化时,这种非类型模板参数的值或者是用户提供的,或是编译器推断的。但是这些值都必须是常亮表达式,因为实例化这些模板是编译器在编译的时候来实例化。
template <typename T ,int a ,int b>
int funcadd3(T c);
int result = funcadd3<int , 12 , 13>(13)
template <unsigned L1, unsigned L2>
int charscomp(const char (&p1)[L1], const char(&p2)[L2])
int result3 = charscomp("abc","acs");//没有提供非类型模板参数,系统会根据长度自动取代L1,L2
编译器不会因为模板定义生成代码,只有在调用模板时,编译器实例化一个特定的函数之后才会生成代码。
模板函数没有重定义的模板,其他函数之类的有
类模板
用类模板实例化一个特定的类;
编译器不能为类模板推断模板参数类型,所以为了使用类模板,必须在模板名后边用<>来提供额外的信息;
vector实现同一套代码应付不同的数据类型;
template<typename 形参名1,typename形参名2,....typename形参名n>
class 类型
{
}
```VECTOR
实例化类模板的时候,必须要有类的全部信息,包括类模板中成员函数的所有信息
定义.h
```cpp
#ifndef __MYVECTOR__
#define __MYVECTOR__
template<typename T>//名字为T的模板参数,用来表示myvector这个容器所保存的元素类型
class myvector
{
public:
typedef T* myiterator;//迭代器
public:
myvector();//构造函数
myvector &operator=(const myvector&);//赋值运算符重载,在类模板内部使用模板名并不需要
public:
myiterator mybegin();//迭代器的起始位置
myiterator myend();//结束位置
}
#endif
实例化
myvector是类模板名,不是一个类名,类模板是用来实例化类用的;
所以myvector才是类名,实例化的类型总会用<>表明类型
myvector<int> tmpvec;//编译器生成了一个具体的类
myvector<double> tmpvec2;
myvector<string> tmpvec3;
类模板的成员函数
类模板成员函数,可以写在类模板定义中,这种写在类模板定义中的成员函数会被隐式声明为inline函数;
类模板一旦被实例化,那么这个模板的每个实例都会有自己版本的成员函数;
所以,类模板的成员函数具有和这个类模板相同的模板参数(类模板的成员函数是有模板参数的);
定义类模板之外的成员函数必须以关键字template开始,后边接类模板参数列表,同时,在类名后边要用<>把模板参数列表里的模板参数名列出,多个用逗号分隔;
写到类外部的成员函数:
tmplate<typename T>
void myvector<T>::myfunc()
{
return;//
}
一个类模板,虽然可能有很多成员函数,但当实例化模板之后,如果后面没有使用到某个成员函数,则这个成员函数不会被实例化(成员函数只有在使用的时候才会被实例化)
模板类名字的使用
myvector<T>& myvector<T>::operator=(const myvector)
非类型模板参数
//类中定义
trmplate<typename T, int size = 10>
class myarray{
private:
T arr[size]:
}
void myarray<T,size>::myfunc()
{
}
//调用
myarray<int,100> tmparray;
tmparray.myfunc();
注意限制:
1)浮点型一般不能做非类型模板参数
2)类类型也不行
typename 的使用场合
1)在模板定义中,typename后面为类型参数
函数模板
template <typename T,int a,int b>
int funcaddv2(T,c){};
类模板
template <typename T>
class myvector{};
2)使用类的类型成员,用typename标明这是一个类型
::作用域运算符
类名::静态成员名
typename myvector<T>::myiterator myvector<T>::mybegin()
{
}
::还可以用来访问类成员
通知编译器,一个名字是一个类型(myiterator)
函数指针做其他函数的参数
typedef int (*FunType)(int,int)://定义一个函数指针类型
int mf(int tmp, int tmp2)
{
return 1;
}
void testfunc(int i,int j,FunType funcpoint)//funcpoint就是函数mf指针?
{
int result = funcpoint(i,j);//相当于调用函数,mf
cout << result << endl;
}
//调用
testfunc(3,4,mf)
函数模板用法举例
template<template T,template F>
void testfunc(const T &i, const T &j, F funcpoint)
{
cout << funcpoint(i,j) << endl;
}
//调用
testfunc(3,4,mf);
可调用对象
//接上面的代码
class tc
{
public:
tc(){cout << "构造函数"<< endl;}
tc(const tc& t){cout << "拷贝构造函数" <<endl;}
//重载圆括号
int operator(){int v1, int v2}const
{
return v1 + v2;
}
}
tc tcobj;//定义对象,调用构造函数
testfunc(3,4,tcobj);//对象作为参数,将对象作为参数传入函数时调用拷贝构造函数,这里能识别出是类类型
//或者直接
testfunc(3,4,tc());//生成的临时对象直接被识别,只执行了构造函数,没有拷贝的过程
默认模板参数
a)类模板,类模板名必须用<>提供额外的信息,<>表示这是一个模板(强调作用)
myarray<> abc;//空的<>表示是类模板,而且完全用模板的缺省参数
myarray<int > def;//第一个参数是int,第二个参数是缺省值
b)函数模板:
template<template T,template F=tc>
void testfunc(const T &i, const T &j, F funcpoint=F())//生成tc对象
{
cout << funcpoint(i,j) << endl;
}
1)同时给模板参数和函数参数提供缺省值
2)注意写法F function = F()
3)tc重载()
成员函数模板
不管是普通类,还是类模板,它的成员函数可以是一个函数模板,成为成员函数模板。
class A
{
public:
template <typename T>
void myft(T tmpt)//成员函数模板
{
cout << tmpt << endl;
}
}
A a;
a.myft(3);//编译器会实例化函数模板,根据传入内容自动判断类型
类模板的成员函数模板
类模板的模板参数必须用<>指定,成员函数
template <typename C>//类模板
class A
{
public:
template <typename T2>
A(T2 v1, T2 v2)//构造函数也引入自己的模板,和整个类的模板C没有关系
{
}
template <typename T>
void myft(T tmpt)//成员函数模板
{
cout << tmpt << endl;
}
void myfc()//普通成员函数
{
}
}
A<float>a(1,2)//类模板指定类型,调用构造函数,构造函数模板自动识别类型
类模板C,构造函数模板T2,普通成员函数模板T互相无影响
//写在类外的成员函数模板
template <typename C>//类的模板参数列表
template <typename T2>//构造函数的模板参数列表
A<C>::A(T2 v1, T2 v2)//
{
}
//调用
A<float>a(1,2);//整形实例化构造函数
A<float>a2(1.1, 2.2);//浮点数实例化构造函数
类模板的成员函数(包括普通成员函数/成员函数模板)只有为程序所用(代码中出现对该函数的调用)才进行实例化,如果某函数从未使用,则不会实例化该成员函数
为了防止在多个.cpp文件中都实例化相同的类模板,所以有显示实例化:通过显示实例化避免通过生成多个类模板带来的开销;
template A<float>;//实例化定义,这种只需要在一个.cpp文件中写明
extern template A<float>;//实例化声明,其他.cpp文件中这样写,不会再对模板进行实例化
模板的实例化定义只有一个,实例化声明有多个。
using定义模板别名
typedef:一般用来定义类型别名
typedef unsigned int unit_t;//给unsigned int 类型起了一个别名unit_t
unit_t abc;
typedef std::map<std::string.int> map_s_i;//定义键值对,字符型加整形,别名map_s_i
map_s_i mymap;
mymap.insert({"first",1});
mymap.insert({"second",2});
//要定义一个类型,键值对为固定类型加不固定类型std::map<std::string,自定义>
//c++98的写法
template <typename wt>
struct map_s;//定义了一个结构、类,只是结构的成员缺省都是public
{
typedef std::map<std::string.wt> type;//定义一个类型
};
map_s<int>:://调用结构中的内容用域标识符,整体等价于std::map<std::string.int> map1;
map1.insert({"first",1})
//c++11
template <typename T>
using str_map_t = std::map<std::string ,T>;//str_map_t是类型别名
str_map_t<int>map2;
map2.insert({"seconf",1});
using用来给类型模板起别名;
using在用于定义类型和定义类型模板的时候是包含了typedef的所有功能,只是两者的顺序相反
typedef unsigned int unit_t;//等价于
using unit_t = unsigned int;
typedef std::map<std::string,int> map_s_i;//等价于
using map_s_i = std::map<std::string,int>;
typedef int(*FunType)(int,int);//定义函数指针
using FunType = int(*)(int,int);//像是赋值
//类型模板
显式指定模板参数
template <typename T1, typename T2, typename T3>
T1 sum(T2 i, T3 j)
{
T1 result = i + j;
return result;
}
//调用函数模板
auto result = sum(2000000000,2000000000);//T2T3的类型系统会自动推断,但是T1不能自动推断会报错
auto result = sum<int>(200000000,200000000);//需要显式指定模板类型,相加后超过int极限,也会报错
auto result = sum<double>(200000000,200000000);//一般情况下正确,此情况下报错
auto result = sum<double,double,double>(200000000,200000000);//从左到右按顺序全部指定类型,正确
模板化
类模板特化
特化:对特殊的类型进行特殊的对待
全特化
所有的类型模板都用具体的类型代表
1)常规全特化
//泛化版本(只要特化要先泛化)
template<typename T,typename U>
struct TC
{
void functest()
{
//
}
}
//当T和U模板参数都为int类型时的特化版本
template<>//全特化,所以为空
struct TC<int,int>//上边的T和C绑定到int上
{
void functest()
{
//
}
}
//调用
TC<char,int> tcchar;
tcchar.functest();
特化版本代码编译器会优先选择,覆盖泛化版本
2)单独特化成员函数
template<>//全特化,所以为空
struct TC<double,double>::functest()
{
}
//调用
TC<double,double> tdbldbl;//
偏特化
1)从模板数量上进行偏特化
//泛化版本
template <typename T, typename U, typename W>//三个类型模板参数
struct TC
{
void functest()
{
//
}
}
//偏特化:绑两个,留一个
template <typename U>//其他两个绑定到具体类型,所以这里只剩下一个U类型模板参数
struct TC<int, U, double>//跳着来
{
void functest()
{
}
}
//
TC<double,int ,double>tcdi;//泛化
tcdi.functest();
TC<int,int,double>tedi;//偏特化
tedi.functest();
b)范围上偏特化:
int(大), const int(小);
T(大),T*(小);
T(大),T&(小)
//泛化版本
template<typename T>
struct TC
{
void functest()
{
}
}
//偏特化版本
template<typename T>
struct TC<const T>//const特化版本
{
void functest()
{
}
}
//
template<typename T>
struct TC<T*>//指针版本(如果使用指针则使用此版本)
{
void functest()
{
}
}
//
template<typename T>
struct TC<T&>//左值引用
{
void functest()
{
}
}
//
template<typename T>
struct TC<T&&>//右值引用
{
void functest()
{
}
}
//调用
TC<double >tc;//调用泛化版本
tc.functest();
TC<double *>tc;//调用偏特化T*版本
tc.functest();
TC<const double >tc;//调用const版本
tc.functest();
TC<const double *>tc;//调用的是*版本
tc.functset();
局部特化本质上还是个模板,但是全特化之后不再是模板
函数模板特化
函数模板只能全特化,不能偏特化
//泛化版本
template<typename T,template U>
void tfunc(T &tmprv, U&tmprv2)
{
}
//调用
const char *p = "abc";
int i = 12;
tfunc(p,i);//T = const char *, tmprv = const char *&, U = int, tmprv2 = int&
//全特化版本
template<>//空
void tfunc(int &tmprv, double &tmprv2)//int和double替换了原来的T和U
{
}
//调用
int i = 12;
double db = 15.6f;
tfunc(i,db);
全特化函数模板实际上等价于实例化一个函数模板,病不是等价于一个函数重载;
当两者都存在时,选择顺序为函数重载、特化版本、泛化版本
//全特化
void tfunc<int,double>(int &,double &){}
//重载函数
void tfunc(int &tmprv, double &tmprv2)
模板特化版本放置位置建议
模板定义,实现都放在一个.h文件中;
模板的特化版本和模板的泛化版本都应该放在一个.h文件中;
.h文件中前边放泛化版本,后边放特化版本
可变参模板
允许模板中含有0个至任意个模板参数
可变参函数模板
a)args一般称为一堆参数,参数的类型可以各不相同
b)一堆参数这种可以容纳0到多个模板参数,而且可以为任意类型
c) T后面带…所以称T为可变参类型,实际上包含了一堆类型;
args代表一堆形参
template <typename... T>//三个点
void myfunc(T... args)//三个点
{
cout << sizeof...(args) << endl;//打印参数个数
cout << sizrof...(T) << endl;//打印参数类型
}
void func()
{
myfunc();
myfunc(1,2,3,5,"abc");
}
template<typename T,typename...U>
void myfunc(const T&firstarg, const U&... otherargs)
{
cout << sizeof...(otherargs) << endl;//打印其他形参的数量
}
//
myfunc(10);
myfunc(10,"abc");
参数包的展开:
一个参数加一包参数的写法最适合参数包的展开;
//递归终止
void myfunc()//当参数为空的时候回调用这个重名函数
{
cout << "递归终止函数" << endl;
}
template<typename T,typename...U>
void myfunc(const T&firstarg, const U&... otherargs)
{
//cout << sizeof...(otherargs) << endl;//打印其他形参的数量
cout << "收到的参数值为" << firstarg << endl;
myfunc(otherargs...);//使用递归函数,不断将除了第一个的重新输入
}
可变参类模板(使用少)
允许模板定义中含有0到任意个模板参数
1)通过递归继承方式展开参数包
template<typename...Args> class myclassst;//主模板
template <typename First, typename... Others>
class myclassst<First,Others...>:private myclassst<Others...>//偏特化
{
public:
myclassst():m_i(0)
{
//
}
myclassst(First part,Others..paro):m_r(part),myclassst<Others...>(paro...)
{
//
}
First m_i;
}
//
void func()
{
myclassst<int,float,double> myc(12,13.6f,23);
}
2)通过递归组合方式展开参数包
组合关系:A中包含B对象
class B
{
}
class A
{
B b;
}
template<typename...Args> class myclassst;//主模板
template <typename First, typename... Others>
class myclassst<First,Others...>:private myclassst<Others...>//偏特化
{
public:
myclassst():m_i(0)
{
//
}
myclassst(First part,Others..paro):m_r(part),myclassst<Others...>(paro...)
{
//
}
First m_i;
myclassst<Others>m_o;//组合关系:参数多的包含参数少的
}
//
void func()
{
myclassst<int,float,double> myc(12,13.6f,23);
}
3)通过tuple和递归调用展开参数包
4)总结
获取参数包里参数的方式有很多种:一般离不开递归手段
模板模板参数
首先是个模板参数,参数本身又是一个模板
template<
typename T,//类型模板参数
typename<class>class Container >//模板模板参数
//template <typename W>typename Container//相同
class myclass
{
public:
T m_i;
Container<T> myc;//Container作为类模板使用(因为有<>)
}