const 的三种用法
(1)const int *p;
const在类型前,称指向常量的指针,可以这样理解它的功能,因为const在int前,所以p指向的这个int变量对于 * p来说是const的,即不能通过* p改变这个变量的值,但是变量本身可以随便改变自己的值。另外也可以改变p的指向。
例:
int x=2;
int y=3;
const int *p=&x;
*p=4; //错误,不能通过*p改变变量的值
x=4; //可以,x不是一个常量,可以改变自己的值
*p=&y; //可以,指针p本身不是一个常量,可以改变p的指向。
(2)int * const q; const在指针q的前面,叫常量指针,也就是说指针q本身是个常量,不能改变q的指向。但是可以通过*q改变所指向变量的值。
例:
int x=2;
int y=3;
int * const q=&x; //注:因为指针q是常量,在声明时就应该初始化。
*q=4; //可以
x=4; //可以,x不是一个常量,可以改变自己的值
*q=&y; //错误,指针q本身是一个常量,不能改变它的指向。
(3)const int * const pq; 这是前两种的结合,叫指向常量的常量指针。功能也是前两者的结合,即不能改变pq的指向,也不能通过*pq改变所以变量的值。
例:
int x=2;
int y=3;
const int * const pq=&x;
*pq=4; //不可以,不能通过*pq改变所指变量的指。
x=4; //可以,x不是一个常量,可以改变自己的值
*pq=&y; //错误,指针pq本身是一个常量,不能改变它的指向。
总结:从上面可以看出,不论那种情况,变量本身都是可以改变自己的值的,除非变量本身就是声明成常量(const int x=2;)。区别三种情况关键就是看const在类型前还是在指针前,在类型前,不能通过*p改变变量的值,在指针前不能改变指针的指向,且在声明时就要初始化。
重载运算符
是对象之间的关系可以用成员函数实现
重载=运算符(重要)
<< ++ –
前提:
类
中
有
指
针
成
员
\color{#FF3030}{类中有指针成员}
类中有指针成员必须重载=运算符默认浅拷贝就可以完成
如果没有指针成员就不需要重载赋值运算符
Person ob2=ob1;
调用的不是赋值运算 而是 拷 贝 函 数 , 旧 对 象 给 新 对 象 初 始 化 \color{#FF3030}{拷贝函数,旧对象给新对象初始化} 拷贝函数,旧对象给新对象初始化默认拷贝构造就是单纯的赋值
Person ob3;//无参构造被屏蔽必须手动实现
ob3=ob1;//这里是赋值,旧对象给新对象赋值,默认是浅拷贝
class Person
{
public:
char *name;
Person(char* name)
{
cout << "有参构造" << endl;
}
~Person()
{
cout << "析构函数" << endl;
}
Person(const Person& ob)
{
cout << "拷贝构造函数" << endl;
this->name = new char[strlen(ob.name) + 1];
strcpy(this->name, ob.name);
}
Person& operator =(Person& ob)
{
if (this->name != NULL)
{
delete[] this->name;
this->name = NULL;//释放以前的空间
}
//申请空间
this->name = new char[strlen(ob.name) + 1];
//拷贝内容
strcpy(this->name, ob.name);
return *this;
}
};
重载!= 和= 运算符
public:
char* name;
bool operator==(Person & ob)
{
if (strcmp(this->name, ob.name) == 0)
{
return true;
}
else return false;
}
函数调用符()的重载(了解)
int operator()(int x, int y)
{
return x + y;
}//在类Fun中
Fun fun;
fun.operator()(100, 200);
//优化 fun和小括号结合
cout << fun(100, 200) << endl;//输出结果为300
//此处Fun 是类名称
//Fun()是个匿名对象
cout << Fun()(100, 200) << endl;
注意: 不是一个函数 仅仅是fun和()结合 调用了()重载运算符,因此把fun(100,200)叫做一个
仿
函
数
\color{#FF3030}{仿函数}
仿函数
仿函数的调用是对象名和()结合
不要重载&&、||(用户无法实现短路特性)
符号重载总结
= ->操作符只能通过成员函数进行重载 << >> 只能通过全局函数配合友元函数进行重载,因为他的左边不是对象 不要重载&& 因为无法实现短路规则
运算符 | 建议使用 |
---|---|
所有的一元运算符 | 成员 |
= () [] -> ->* | 必须是成员 |
+= -= /= *= ^=&= != %= >>= <<= | 成员 |
其他二元运算符 | 非成员 |
强化训练运算符 字符串类string
重载[]运算符 返回值必须是左值 返回值必须是引用
Mystring str1"hello world";
cout<<str1[1];
char& Mystring::operator[](int index)
{
if (index >= 0 && index < this->size)
{
return this->str[index];
}
else
{
cout << "index无效 " << endl;
}
}
重载类型转化符
operator int()
{
return (int)real;
}
前面不可以有返回类型,数据转化函数,C++提供类型转换函数(type conversion function)来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据。
一般形式:
operator 类型名( )
{
实现转换的语句
}
s t r c a t 用 于 拼 接 两 个 字 符 串 常 量 \color{#FF3030}{strcat 用于拼接两个字符串常量} strcat用于拼接两个字符串常量
重载+运算符 用于两个字符串常量拼接
Mystring& operator+( const Mystring&ob)
{
int newsize = this->size + ob.size;
char* tmp_str = new char[newsize];
//清空tmp_str所指向的空间
memset(tmp_str, 0, newsize);
strcpy(tmp_str, this->str);
strcat(tmp_str,ob.str);
static Mystring newString(tmp_str);//局部对象不可以用引用???
//释放tmp_str所指向的临时空间
if (tmp_str != NULL)
{
delete[]tmp_str;
tmp_str = NULL;
}
return newString;
}
bool operator==(const char* str)
{
if (strcmp(this->str, str) == 0)
{
return true;
}
else return false;
}
继承与派生
三个关键字的访问范围:
public:能被类成员函数,子类函数,友元访问,也能被类的对象访问
private:只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行
protected:只能被类成员函数,子类函数以及友元访问,不能被其他任何访问,本身的类对象也不行
基类就是父类,派生类就是子类
class NewsPage:publuc IndexPage
{
};
子类继承父类体现了共性,但子类又具有个性
单继承:一个父类派生一个子类
多继承:多个父亲
子类继承父类的全部成员,(构造和析构函数除外)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l2x6RIUU-1625473554485)(\Users\86156\Pictures\onlineclass.png)]
构造和析构函数的顺序
父类 对象成员 子类本身的构
子类对象成员 父类
子类默认调用父类的无参构造,想要调用有参构造,要显示调用,用初始化列表
对象默认是浅拷贝,深拷贝是无法继承的
菱形继承
多继承容易产生菱形继承,产生二义性
解决二义性可以增加作用域 还可以用虚基类
虚继承
继承的动作 虚继承
继承的基类 虚基类
class 子类:virtual public 父类
当子类虚继承父类时为什么会增加空间呢
增加了一个vbptr虚基表指针 虚继承时,虚基类指针vbptr指向虚基类表vbtable,虚基类表中存放的就是数据相对于虚基类指针的偏移,从而根据偏移找到数据
vbptr ==> vbtable
产生vbptr 和vbtable 的目的是不管有多少个继承 虚基类的数据只有一份
vbtable 保存了当前的虚指针和对于虚基类的首地址的偏移量
多态
c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是静态多态。而派生类和虚函数 实现运行时多态。
静态和动态的区别就是函数地址是早绑定(静态联编)还是晚绑定函数的入口/调用地址不能在编译期间确定,而是需要在运行时才能决定(动态联编)。
定义一个基类指针申请了 cat 保存了cat的起始地址
指针的类型是animal 所以说可以指向子类 但animal只能操作animal的那一部分 animal *p=new cat 向上转换
基类指针、引用指向子类对象 安全
子类指针、引用 指向基类对象
怎么用基类指针保存子类对象同时操作子类成员呢
使用虚函数
正常情况下,基类指针、引用只能访问子类对象中基类部分数据
只要涉及到继承 子类中同名函数 都是虚函数
##使用基类指针、引用访问子类对象中的成员方法(虚函数)
使用virtual修饰成员函数,该成员函数就是虚函数。
虚函数会产生一个虚指针vfptr,指向虚函数表vftabale,表中存放的是一个个虚函数的入口地址(普通成员函数不占类的空间) 虚函数的本质是一个函数指针变量
如果 没有涉及到继承,函数指针变量 就指向自身,这样就没有意义
拥有虚函数的类涉及到继承
当虚函数涉及到继承的时候,子类会继承父类的vbptr和vfptr编译器会将虚函数表中的函数入口地址 更新成子类的同名 (返回值、参数都相同)的函数入口地址。
如果基类指针、引用 访问虚函数的时候 就会间接的调用子类的虚函数
C++的动态捆绑机制是怎么样的?
首先 我们看看编译器如何处理虚函数,当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且增加一个指针vptr,这个指针是指向对象的虚函数表,在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
Animal 类型的指针P 在没有涉及虚析构的时候 只能调用父类的析构函数,解决这个问题要用虚析构(虚函数)
虚析构作用:通过基类指针、引用释放子类所有空间
在析构函数前加virtual
作用:为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
子类析构调用完,自动调用父类析构
虚函数实现多态的条件
► ①类之间的继承关系满足赋值兼容性规则;
► ②改写了同名的虚函数;
► ③根据赋值兼容性规则使用指针(或引用)。
满足前两条并不一定产生动态联编,必须同时满足第3条才能保证实现动态联编.
在虚函数中使用成员名限定,可以强制解除动态联编
class A
{
public:
virtual int get()
{return 0;}
};
class B:public A
{
public:
int get()
{return 1;}
};
int main()
{
B b;
A *p=&b; //子类对象赋给基类指针 ,由于父类是个虚函数,故动态联编,调用子类自身的同名函数
cout<<p->get()<<endl; //动态联编, 调用子类的同名函数
cout<<p->A::get()<<endl; //使用成员名限定可以强制解除动态联编, 调用父类的函数
cout<<"-------------"<<endl;
B *p1=&b; //子类对象赋给子类自身类型的指针
cout<<p1->get()<<endl;
return 0;
}
运行结果:
1
0
-------------
1
Press any key to continue
简单来说就是加上作用域标识符
纯虚函数 抽象类
virtual void sleep(void)=0;//不是真正的赋值0
如果一个类中有纯虚函数,那么这个类叫
抽
象
类
\color{#FF3030}{抽象类}
抽象类 不可以实例化对象 但可以实例化对象指针 Animal *p
抽象类派生子类,子类必须实现所有的纯虚函数,否则子类也是抽象类
抽象类就是提供一个固定的流程和接口 ,不需要实现函数体
接口继承
多继承有争议,接口继承毫无争议,接口类只是一个功能声明,并不是功能实现,子类根据功能说明定义功能实现,注意:除了析构函数外,其他声明都是纯虚函数
###纯虚析构函数
class B
{//1、
virtual ~B ()=0;
}
//2、
B::~B(){}//必须有析构函数的函数体
//原因:通过基类指针 释放了子类对象时,先调用了子类析构,再父类析构(如果父类的析构不实现,无法实现调用)
虚函数 纯虚函数 虚析构 纯虚析构
- 虚函数:只是virtual修饰,有函数体(作用于成员函数)
目的:通过基类指针或引用操作子类的方法
class B
{
public:
virtual my_fun(void)
{
//有函数体
}
};
- 纯虚函数:virtual 修饰加=0 没有函数体,所在的类为抽象类
目的:提供固定流程和子类的接口
class B
{
public:
virtual my_fun(void)=0;
};
- 虚析构:virtual 修饰类中的析构函数
目的:用基类指针删除派生类对象
class B
{
public:
virtual ~B()
{
//有函数体
}
};
- 纯虚析构
目的:同上删除派生对象
class B
{
public:
virtual ~B()=0;
};
B::~B()
{
}
重写 重载 重定义 (了解)
- 重载
1.1 同一作用域,参数个数,参数顺序,参数类型不同
1.2和函数返回值没有关系
1.3const 也可以作为重载条件 归属为 参数类型不同 - 重定义
1.1 首先要有继承
1.2 子类重新定义父类的同名成员 (非virtual函数)
class Base
{
public:
void fun(int ){}
void fun (int ,int){}
}
class Son :public Base
{
public:
void fun (){}//同名,参数可以不同,返回值可以不同
}
- 重写(覆盖)
1.1 有继承
1.2 子类重写父类的virtual 函数
1.3 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class Base
{
public:
virtual void fun(int){}
}
class Son :public Base
{
public:
virtual void fun (int){}//完全一致
函数模板
也是可以重载的
函数模板经过两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
模板的局限性
如果T是数组,就不能进行赋值操作,进行比较时,比较的是地址之间的关系。如果T是结构体,就无法判断两个变量之间的大小关系。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供具体化的模板。
C++类型转换
6.1 静态转换(static_cast)
6.2 动态转换(dynamic_cast)
6.3 常量转换(const_cast)
6.4 重新解释转换(reinterpret_cast)
静态转换(static cast)
al 函数
1.3 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class Base
{
public:
virtual void fun(int){}
}
class Son :public Base
{
public:
virtual void fun (int){}//完全一致
函数模板
也是可以重载的
函数模板经过两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
模板的局限性
如果T是数组,就不能进行赋值操作,进行比较时,比较的是地址之间的关系。如果T是结构体,就无法判断两个变量之间的大小关系。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供具体化的模板。
C++类型转换
6.1 静态转换(static_cast)
6.2 动态转换(dynamic_cast)
6.3 常量转换(const_cast)
6.4 重新解释转换(reinterpret_cast)