文章目录
类
概述:类的基本思想是数据抽象和封装
class strudent {
public:
void printId() const {std::count << "test"};
private:
};
定义成员函数
类的成员函数是函数的一种,它的用法和作用和一般函数基本上是一样的,它也有返回值和函数类型。它与一般函数的区别只是:它是属于一个类的成员,出现在类体中。它可以被指定为private(私有的),public(公用的)或protected(受保护的)。
在使用类成员函数的时候,要注意调用它的权限(即,它能否被调用)以及它的作用域(函数能使用什么范围中的数据和函数)。例如,私有的成员函数只能被本类中的其他成员函数所调用,而不能被类外调用。成员函数可以访问本类中任何成员(包括私有的和公用的),可以引用在本作用域中有效的数据。
class strudent {
public:
void test() const {std::count << "test"};
private:
};
其中test 为 strudent类的成员函数
上述是在类内定义 也可以在类外定义如下:
class strudent {
public:
void test() const;
private:
};
void strudent::test const(void) {
std::count << "test";
}
this 指针
C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在该函数体所有成员变量的操作,都是通过该指针去访问,只不过所有的操作对于用户是透明,即用户不需要来传递,编译器自动完成
即在函数内部可以通过this 指针访问其他成员
public
public(公用的)
定义在public说明符之后的成员在整个程序内可被访问
private
private(私有的)
定义在public说明符之后的成员可以被类的成员函数访问
protected
protected(受保护的)
保护成员的可访问范围比私有成员大,比公有成员小。能访问私有成员的地方都能访问保护成员
派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员
构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员来控制其他对象的初始化过程,这些函数叫做构造函数。
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数
构造函数的名字和类名相同,构造函数没有返回类型
类可以包含多个构造函数,和其他重载函数差不多
构造函数初始值列表
带有成员初始化表的构造函数的一般形式如下:
类名::构造函数名([参数表])[:(成员初始化表)]
class myclass{
public:
int a,b,c;
myclass(): a(1),b(2),c(3)
{
}
myclass(int x, int y, int z): a(x),b(y),c(z)
{
}
};
注意:构造函数对常量成员变量和引用成员变量进行初始化
函数重载概念
函数重载即函数名相同,函数形参列表不同(函数特征标不同)的一类函数称为函数重载
如果两个函数的参数数目和类型相同,那么这两个函数的函数特征标就相同,不能重载。
如下举例一组重载函数:
void fun(int a);
void fun(int a,int b);
void fun(double a,int b);
void fun(double a,double b);
void fun(const char* str);
void funn(char* str);
注意:函数的返回类型和重载没有任何关系,即两个重载函数的函数返回类型可以不同,也可以相同。
当函数重载遇上引用参数的时候,我们就需要特别注意一下。
如:
void fun(int x);
void fun(int& x);
形参列表不同,看起来可以函数重载,但是当我们调用的时候,编译器看不出来不同。比如我们调用函数:
int a = 10;
fun(a);
这个时候编译器就不知道该调用哪一个函数,因为参数a和两个fun函数形参int x和int& x都匹配,所以编译器这个时候不知道该选用哪一个函数,就会出错。
编译器在检查函数特征的时候,会把类型引用和类型本身看成是同一个函数特征标
类拷贝
拷贝:
举例:Box box1(box2) 或者 Box box1=box2; (对象box2之前已经定义);
- 类类型成员,使用其拷贝构造函数拷贝
- 内置类型成员:直接拷贝
- 数组:逐元素拷贝
- 拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量。
- 在自己未主动定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数(“位拷贝”)——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。
- 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。
简而言之,当数据成员中有指针时,必须要用深拷贝
以类 String 的两个对象 a, b 为例,假设 a.m_data 的内容为“hello”,b.m_data 的内容为“world”。现将 a 赋给b,缺省赋值函数的“位拷贝”意味着执行 b.m_data = a.m_data。这将造成三个错误:一是 b.m_data 原有的内存没被释放,造成内存泄露;二是 b.m_data 和 a.m_data 指向同一块内存,a 或 b 任何一方变动都会影响另一方;三是在对象被析构时,m_data 被释放了两次。
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数时拷贝构造函数
(使用引用是因为,如果不引用,调用拷贝构造函数时形参会拷贝实参,但是拷贝实参又要调用拷贝构造函数,无限循环)
class Foo {
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
private:
int num;
};
带有指针的深拷贝
class Foo {
public:
Foo(int n): num(n){
str = new char[n];
}; //默认构造函数
Foo(const c&) {
num = c.num
str = new char[num];
if (str) {
strcpy(str, c.num);
}
}; //深拷贝构造函数
private:
int num;
char * str;
};
类赋值
类的拷贝(复制)针对从无到有新创建的对象,类赋值是针对已存在的对象。
举例:Box box1(1, 2, 3), box2(4, 5, 6); box1 = box2;
赋值函数只能被已经存在了的对象调用
析构函数
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数名是在类名前加上字符 ~
析构函数在类对象生命周期结束时由编译器自行调用
注意:析构函数不能重载
使用场景:当对象申请了其他内存资源时,对象销毁前必须先释放其申请的内存资源
class testClass {
public:
testClass(){ str = new char[100]};
~testClass(){delete str;};
private:
char * str;
};
友元
友元是一种权限授予机制,它允许一个类或函数访问另一个类中的私有成员(私有成员变量和私有成员函数)
如果想把一个函数作为它的友元,只需要增加一条以friend关键字开始的声明语句即可
class FriendClass; // 前向声明,表示FriendClass是一个类,但不定义其成员
class TargetClass {
public:
// 公有成员和函数声明
private:
// 私有成员和函数声明
friend class FriendClass; // FriendClass成为TargetClass的友元
};
在上面的代码中,FriendClass
成为了TargetClass
的友元,从而可以访问TargetClass
中的私有成员。
除了友元类,我们还可以将一个全局函数声明为类的友元。全局函数在声明时要加上类的作用域限定符:
class TargetClass {
public:
// 公有成员和函数声明
private:
// 私有成员和函数声明
friend void friendFunction(TargetClass& obj); // friendFunction成为TargetClass的友元
};
全局函数friendFunction
被声明为TargetClass
的友元,允许其访问TargetClass
的私有成员。
友元函数
友元函数是声明为另一个类的友元的全局函数。在C++中,友元函数可以访问其所属类的私有成员,但不是类的成员函数。友元函数没有隐含的this指针,因此不能直接访问类的非静态成员变量
class TargetClass;
class FriendClass {
public:
void friendFunction(TargetClass& obj); // 声明TargetClass为友元类
void normalFunction(TargetClass& obj) {
// 友元函数可以访问TargetClass的私有成员
int x = obj.privateVar;
}
};
class TargetClass {
public:
TargetClass(int val) : privateVar(val) {}
private:
int privateVar;
friend void FriendClass::friendFunction(TargetClass& obj); // 声明friendFunction为友元函数
};
void FriendClass::friendFunction(TargetClass& obj) {
// 友元函数可以访问TargetClass的私有成员
int x = obj.privateVar;
}
注意友元成员函数可能会打破封装性和数据隐藏原则,因此需要谨慎使用,并确保在需要的情况下使用友元成员函数
友元类
友元类是指一个类被声明为另一个类的友元。友元类可以访问目标类的私有成员,类似于允许其他类访问自己私有成员的特权。
class FriendClass {
public:
void accessTarget(TargetClass& obj) {
// 友元类可以访问TargetClass的私有成员
int x = obj.privateVar;
}
};
class TargetClass {
public:
TargetClass(int val) : privateVar(val) {}
private:
int privateVar;
friend class FriendClass; // FriendClass成为TargetClass的友元类
};
友元的局限性
- 友元关系不可传递:如果A是B的友元,B是C的友元,那么A并不会成为C的友元。
- 友元关系不可继承:派生类不继承基类的友元,除非它显式地声明自己的友元。
- 友元是单向的:如果类A是类B的友元,不一定意味着类B也是类A的友元。
- 友元不影响类的访问控制:友元只影响类的访问权限,但并不改变成员的实际可见性。
类的静态成员
有些时候类需要一些成员与类本身直接相关,而不是与类的各个对象保持联系。
比如 一个银行账户类可能需要一个数据成员来表示当前的基准利率
注意静态成员函数不与任何对象绑定在一起,它们不包含this指针
声明静态成员
类产生的所有对象共享系统为静态成员分配的一个存储空间,而这个存储空间是在编译时分配的,在定义对象时不再为静态成员分配空间,即静态成员的大小不占对象的空间
在类定义中,用关键字static修饰的数据成员为静态数据成员
静态成员变量的初始化和使用
静态成员变量的初始化是在类外初始化,初始化的语法为
类型名 类名::变量名 = 值
- 类名::静态数据成员名
- 通过类实例化出的对象来进行访问
- 通过成员函数来进行访问(静态函数、非静态函数都可以)
#include <iostream>
#include <istream>
#include <ostream>
#include <string>
using namespace std;
class Maker
{
public:
Maker(int d = 0) :data(d)
{
}
void show()
{
cout << "data:" << data << endl;
cout << "count:" << count << endl;
}
static void print()
{
cout << "count:" << counnt << endl;
}
public:
static int count; // 静态数据成员
private:
int data;
};
int Maker::count = 0; // 静态数据成员类外初始化
int main()
{
Maker m;
m.show(); // 通过普通成员函数访问静态成员变量
m.print(); // 通过静态成员函数访问静态成员变量
cout << Maker::count << endl; // 通过类的作用域访问静态成员变量
cout << m.count << endl; // 通过实例化的对象访问静态成员变量
// sizeof(m) = 4,静态成员变量不占有类的空间
cout << "size:" << sizeof(m) << endl;
system("pause");
return 0;
}
静态成员函数
函数成员说明为静态,将与该类的不同对象无关,与静态数据成员相反,为使用方便,静态函数成员多为公有的,静态函数的调用与静态变量的调用类似,在对象之外
可以通过作用域运算符进行调用
类名::函数名(参数)
静态成员函数的性质
一个常规的成员函数声明描述了三件在逻辑上相互不同的事情
1、该函数能访问类声明的私用部分
2、该函数位于类的作用域之中
3、该函数必须经由一个对象去激活(有一个this指针)
但是通过将函数声明为static,可以让它只有前两种性质,即静态成员函数由同一类的所有对象共享,它不具有指向某一具体对象的this指针,即不能通过静态函数访问普通的成员变量和成员函数,静态函数只能管理类中的静态成员和静态函数
class Maker
{
public:
Maker(int d = 0) :data(d)
{
}
void show()
{
cout << "data:" << data << endl;
cout << "count:" << count << endl;
}
static void print()
{
// show函数为普通的成员函数,在调用时需要传入this指针
// 即在调用时会被改写为 show(Maker *const this)
// print为静态成员函数,没有this指针,调用非法
show(); // error 静态函数内不能调用普通的成员函数
cout << count << endl; // right
// error 原因同普通成员函数的调用,需要this指针,但是静态成员函数不提供
cout << data << endl;
}
public:
static int count;
private:
int data;
};
int Maker::count = 0;
基类和派生类
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类
派生类的定义
单继承类:class <派生类名>:<继承方式><基类名> { <派生类新定义成员>}。 其中,<派生类名>是新定义的一个类的名字,它是从<基类名>中派生的,并且按指定的<继承方式>派生的;<继承方式>常使用如下三种关键字给予表示:public 表示公有基类;private 表示私有基类;protected 表示保护基类。
多继承类:class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… { <派生类新定义成员> };
公有继承,私有继承和保护继承
公有继承
我们的派生类不能去访问基类的私有成员,那么我们就可以得出,当类的继承为公有继承时,在派生类中,基类的公有成员和收保护成员被继承后分别作为派生类的公有成员和受保护成员。这也就是为什么可以访问基类的公有成员和受保护成员的原因。
私有继承
当类的继承方式是私有继承时,在派生类中,基类的公有成员和受保护成员作为派生类的私有成员,派生类成员可以访问基类访问他们,但是不能直接昂访问基类的私有成员.
多继承
一个类由多个基类派生的一般形式:
class<派生类名>:<继承方式><基类名1>,~~~~<继承方式><基类名2>
{
<定义派生类的自己的成员>
}
二义性和支配规则
我们在构建不同类的成员的时候都是不同的声明,但是我们在解决某些问题的时候,可能会出现多个基类的成员函数相同,那么我们在访问的时候,会不会出现不确定的情况?这就是我们今天学的二义性
我们先来看一个程序:
#include<iostream>
using namespace std;
class Baseclass1
{
public:
void seta(int x) { a = x; }
void show() { cout << "a=" << a << endl; }
private:
int a;
};
class Baseclass2
{
public:
void setb(int x) { b = x; }
void show() { cout << "b=" << b << endl; }
private:
int b;
};
class Derivedclass :public Baseclass1, public Baseclass2
{
};
int main(void)
{
Derivedclass obj;
obj.seta(2);
obj.show();//出现二义性,不能编译;
obj.setb(4);
obj.show();//出现二义性,不能编译;
}
若要消除二义性,需要使用作用域运算符::;
我们只需要将上面的程序更改如下就可以避免二义性;
obj.Baseclass1::show();
obj.Baseclass2::show();
}
虚基类
如果一个派生类从多个基类派生而来,而这些基类又有一个共同的基类,则在这个派生类中访问这个共同基类中的成员时可能产生二义性.
错误例子:
#include<iostream>
using namespace std;
class Base
{
protected:
int val;
};
class Baseclass1 :public Base
{
public:
void seta(int x) { val = x; }
};
class Baseclass2 :public Base
{
public:
void setb(int x) { val = x; }
};
class Derivedclass :public Baseclass1, public Baseclass2
{
public:
void show() { cout << "val=" << val << endl; } //h含义不清,不能编译;}
};
int main(void)
{
Derivedclass obj;
obj.seta(2);
obj.show();
obj.setb(4);
obj.show();
}
程序分析:该程序中四个类的关系如下图,由于两条继承路径上的成员val
相互之间没有支配关系。
引进虚基类的目的是为了解决二义性问题,使得公共基类在他的派生类对象中只产生一个基类子对象。
虚基类说明格式如下:
virtual<继承方式>基类名
virtual是说明虚基类的关键字。虚基类的说明是在派生类名的后面。在派生类中使用关键字virtual会导致他们共享基类Base的同一个单独公共对象。因此,Base是虚基类。
例子:
#include<iostream>
using namespace std;
class Base
{
protected:
int val;
};
class Baseclass1 :public virtual Base
{
public:
void seta(int x) { val = x; }
};
class Baseclass2 :public virtual Base
{
public:
void setb(int x) { val = x; }
};
class Deviredclass :public Baseclass1, public Baseclass2
{
public:
void show();
};
void Deviredclass::show()
{
cout << "Baseclass val=" << val << endl;
}
int main(void)
{
Deviredclass obj;
obj.seta(3);
obj.show();
obj.setb(4);
obj.show();
}
虚函数
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表
类的虚表
每个包含了虚函数的类都包含一个虚表。
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
也就是说,每一个类都有一份虚表,不管多少个对象,都是用的是同一个虚表
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表
动态绑定
代码:
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
类A是基类,类B继承类A,类C又继承类B
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
虽然看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
也就是说,虚表是虚表,非虚函数不走虚表,基类的指针只能调用基类的方法,但是记住虚函数的指针是基类的,其他子类继承下来也只是继承了使用权
纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:
virtual void function()=0
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
例子:
class base_a {
public:
virtual void vfunc1()=0;
private:
}
class b :public base_a {
public:
void vfunc1() {
std::cout << "print b" << endl;
};
};