【C++笔记6】类和对象

类和对象(封装和对象的初始化和清理)

C++具有面向对象的三大特性:封装,继承,多态。C++认为万事万物都是对象,都具有其行为和属性。任何事物都可以定义属性和行为,那么根据这个特点,我们可以按照以下方法定义。

封装

用以下语句完成封装:class {访问权限: 行为/属性};

封装可以把生活中的事物,用C++抽象出来,写出其属性和行为。并且可以用访问权限加以权限控制{private,protected,public}。

例如以下代码

class Circle
{
    public:
    int r;//半径
    int ZC;//周长
    private:
    protected:
}

在C++中我们把通过一个类创建一个具体的对象的过程叫做实例化。

三种权限在使用过程中分别发挥这不同的作用,譬如说:public权限无论类的内外均有权访问,protected和private在类外无权访问。后两者区别在继承中会有不同。

类比C语言中的struct(结构体),其和class不同的是默认访问权限不同,struct默认为public,class默认为private。

对象的初始化和清理-(构造函数和析构函数)

对于对象而言,初始化和格式化是个非常重要的安全问题,在当今这个对于保护个人隐私非常重要的时代,我们有必要学习一下(无端联想)。

C++语言利用构造函数的析构函数来解决上述问题,这两种函数在没有我们定义的时候会由编译器自动调用,完成初始化和清理工作。

对象的初始化和清理工作是必须要做的事情,如果我们没有提供构造和析构函数,编译器会提供这两个函数为空实现。

  • 构造函数:在创建对象的时候自动调用的函数。
  • 析构函数:在对象被销毁前自动调用,执行一些清理工作。

构造函数语法: 类名() {}

  • 可以有参数,不写void,没有返回值,函数名和类名相同。

析构函数语法:~类名() {}

  • 不可以有参数,不写void,没有返回值,函数名和类名相同。

以下是代码示例和运行结果

//1-1
#include<iostream>
using namespace std;

class Example_e
{
    public:
    Example_e()//构造函数
    {
        cout<<"构造函数调用"<<endl;
    }
    ~Example_e()//析构函数
    {
        cout<<"析构函数调用"<<endl;
    }
};
void example01()
{
    Example_e ex1;
}
int  main()
{
    example01();
    return 0;
}

运行结果:

构造函数调用
析构函数调用

由于ex1是局部变量存放于栈区,在调用结束后就会被自动释放,所以,构造函数和析构函数都被调用了。

构造函数的分类及调用

构造函数分为有参构造,无参构造,普通构造,拷贝构造(复制构造也对)。

在定义构造函数的时候有参数就叫做有参构造。

有以下代码:

//1-2
#include<iostream>
using namespace std;
class example
{
    public:
    example(int a)
    {
        cout<<"现在是有参调用"<<endl;
    }
    example()
    {
        cout<<"现在是无参调用"<<endl;
    }
    //拷贝构造函数
    example (const example &xx)
    {
        cout << "拷贝构造函数!" << endl;
    }
    //析构函数
    ~example() 
    {
        cout << "析构函数!" << endl;
    }
};

void example01()
{
    cout<<"现在调用无参构造函数"<<endl;
    example exp1;
    cout<<"调用完毕1"<<endl;
}
void example02()
{
    cout<<"现在调用有参构造函数"<<endl;
    //第一种调用方式
    example exp2(10);
    //注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
    //example exp2();
    cout<<"完毕2"<<endl;
    //第二种调用方法
    example exp3 = example(10);
    cout<<"完毕3"<<endl;
    example exp4 = example(exp2);
    //2.3 隐式转换法
    example p4 = 10; // example p4 = example(10); 
    example p5 = p4; // example p5 = example (p4); 
    cout<<"完毕4"<<endl;
    //注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
    //example p5(p4);
}
int main()
{
    example01();
    example02();
    return 0;
}

运行结果一目了然:

现在调用无参构造函数
现在是无参调用
调用完毕1
析构函数!
现在调用有参构造函数
现在是有参调用
完毕2
现在是有参调用
完毕3
拷贝构造函数!
现在是有参调用
拷贝构造函数!
完毕4
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!

一行一行读下去,自然就懂了,不懂的话,欢迎Github提交一个issue。

什么时候使用拷贝(复制)构造函数

  • 使用一个已经创建完毕的对象来初始化一个新的对象。

  • 值传递的形式给函数参数传值。

  • 以值的方式传回局部对象。

简简单单地来一个程序示例吧

//1-3-1
#include<iostream>
using namespace std;
class example 
{
    public:
    //无参(默认)构造函数
    example() {
        cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	example(int a) {
		number = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	//example(const example& p) {
        //number = p.number;
		//cout << "拷贝构造函数!" << endl;
	//}
	//析构函数
	~example() {
		cout << "析构函数!" << endl;
	}

    int number;
};
void test01()
{
	example p1(18);
	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	example p2(p1);

	cout << "这个数字是 " << p2.number << endl;
}

int main()
{
    test01();
    return 0;
}

运行结果:

有参构造函数!
这个数字是 18
析构函数!
析构函数!

这个里面我们能看到如果我把拷贝构造函数注释掉,他会以值传递的形式把p1传给p2.

假设我们只提供拷贝构造函数会发生什么呢?

//1-3-2
#include<iostream>
using namespace std;
class example 
{
    public:
	//无参(默认)构造函数
	//example() {
	//	cout << "无参构造函数!" << endl;
	//}
	//有参构造函数
	//example(int a) {
	//	number = a;
	//	cout << "有参构造函数!" << endl;
	//}
	//拷贝构造函数
	example(const example& p) {
        number = p.number;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~example() {
		cout << "析构函数!" << endl;
	}

    int number;
};
/*
void test01()
{
	example p1(18);
	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	example p2(p1);

	cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
    
	//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
	example p1; //此时如果用户自己没有提供默认构造,会出错
	example p2(10); //用户提供的有参
	example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

	//如果用户提供拷贝构造,编译器不会提供其他构造函数
	example p4; //此时如果用户自己没有提供默认构造,会出错
	example p5(10); //此时如果用户自己没有提供有参,会出错
	example p6(p5); //用户自己提供拷贝构造

}
int main()
{
    test02();
    return 0;
}

他是一定会报错的:

类“example”不存在默认构造函数
没有与参数列表匹配的构造函数"example::exmaple"实例

如果我提供有参构造和拷贝构造呢(代码都差不多,就是加了注释)

//1-3-3
#include<iostream>
using namespace std;
class example 
{
    public:
	//无参(默认)构造函数
	//example() {
	//	cout << "无参构造函数!" << endl;
	//}
	//有参构造函数
	example(int a) {
    	number = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	example(const example& p) {
        number = p.number;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~example() {
		cout << "析构函数!" << endl;
	}

    int number;
};
/*
void test01()
{
	example p1(18);
	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	example p2(p1);

	cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
    
	//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
	example p1; //此时如果用户自己没有提供默认构造,会出错
	example p2(10); //用户提供的有参
	example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

	//如果用户提供拷贝构造,编译器不会提供其他构造函数
	example p4; //此时如果用户自己没有提供默认构造,会出错
	example p5(10); //此时如果用户自己没有提供有参,会出错
	example p6(p5); //用户自己提供拷贝构造

}
int main()
{
    test02();
    return 0;
}
类“example”不存在默认构造函数

所以有了有参构造函数,就不会提供默认构造函数(无参)

都提供了,运行结果如下:

//1-3
#include<iostream>
using namespace std;
class example 
{
    public:
	//无参(默认)构造函数
	example() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	example(int a) {
    	number = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	example(const example& p) {
        number = p.number;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~example() {
		cout << "析构函数!" << endl;
	}

    int number;
};
/*
void test01()
{
	example p1(18);
	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	example p2(p1);

	cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
    
	//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
	example p1; //此时如果用户自己没有提供默认构造,会出错
	example p2(10); //用户提供的有参
	example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

	//如果用户提供拷贝构造,编译器不会提供其他构造函数
	example p4; //此时如果用户自己没有提供默认构造,会出错
	example p5(10); //此时如果用户自己没有提供有参,会出错
	example p6(p5); //用户自己提供拷贝构造

}
int main()
{
    test02();
    return 0;
}
无参构造函数!
有参构造函数!
拷贝构造函数!
无参构造函数!
有参构造函数!
拷贝构造函数!
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!

说明了当我们已经定一个拷贝构造函数的时候确实系统不会自动生成一个默认构造函数

构造函数的时候,在默认的情况之下,编译器会自动生成三个函数

默认构造函数,默认析构函数,默认拷贝构造函数。前两者在默认的情况下会默认无参数,函数体为空,不执行任何操作,对于默认拷贝构造函数则会以值传递的形式复制一份。

如果用户已经定义了一个有参构造函数,则不会提供一个默认无参构造函数,但是仍然会提供一个拷贝构造函数。但是如果用户已经定义了一个拷贝构造函数,则程序不会提供其他的构造函数。

深拷贝和浅拷贝

在之前的注释中,我们应该可以看到一个名词叫作浅拷贝,顾名思义,浅的拷贝,就是简单的赋值拷贝操作。深拷贝就是我们要向堆区申请空间,并进行拷贝操作。

请看这个代码:

//1-4
#include<iostream>
using namespace std;
class example
{
    public:
    public:
	//无参(默认)构造函数
	example() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	example(int number01 ,int number02) {
		
		cout << "有参构造函数!" << endl;

		number1 = number01;
		number2 = new int(number02);
		
	}
	//拷贝构造函数  
	example(const example& p) {
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		number1 = p.number1;
		number2 = new int(*p.number2);
		
	}

	//析构函数
	~example() {
		cout << "析构函数!" << endl;
		if (number2 != NULL)
		{
			delete number2;
		}
	}
    int number1;
    int *number2;
};

void test01()
{
	example p1(18, 180);

	example p2(p1);

	cout << "p1数一:" << p1.number1 << "数二" << *p1.number2 << endl;

	cout << "p1数一:" << p2.number1 << " 数二: " << *p2.number2 << endl;
}
int main()
{
    test01();
    return 0;
}

运行结果:

有参构造函数!
拷贝构造函数!
p1数一:18数二180
p1数一:18 数二: 180
析构函数!
析构函数!

这个代码是正确的,假设我们在拷贝构造函数的时候没有开辟新的内存分区,没有使用new,那么会发生什么呢?

实测VSCODE确实可以运行!也确实,这玩意有点问题。

然而同样的代码在Visual studio上不可运行,没有进行深拷贝确实会带来一个非常严重的重复释放的问题。

已执行断点指令。

初始化列表

C++可以用初始化列表来初始化属性。

它的语法是这样的:构造函数():属性1(值1),属性2(值2)... {},以下是一段具体例子

class example {
	public:
	example(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {

	}
	private:
	int m_A;
	int m_B;
	int m_C;
}

可以用来初始化属性,具体可以自行尝试如何使用。

类对象作为成员

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

例如:

class A {}
class B
{
    A a;
}

B类中有对象A作为成员,A为对象成员

///1-5
#include<iostream>
#include<string>
using namespace std;
class example1
{
    public:
    string exp1;
    example1()
    {
        cout<<"example1无参构造"<<endl;
    }
    //1有参构造函数
    example1(string name1)
    {
        exp1=name1;
        cout<<"example1 构造函数调用"<<endl;
    }
    //1析构函数
    ~example1()
    {
        cout<<"example1 析构函数调用"<<endl;
    }
};
class example2
{
    public:
    string exp2;

    example2()
    {
        cout<<"example2无参构造"<<endl;
    }
    //2有参构造函数
    example2(string name2 , string name3)
    {
        exp2=name2;
        ex1=name3;
        cout<<"example2构造函数调用"<<endl;
    }
    //2析构函数
    ~example2()
    {
        cout<<"example2析构函数调用"<<endl;
    }
    void print()
    {
        cout<<"1内容"<<ex1.exp1<<"2内容"<<exp2<<endl;
    }
    example1 ex1;
};
void test01()
{
    //当类中成员是其他类对象时,我们称该成员为 对象成员
	//构造的顺序是 :先调用对象成员的构造,再调用本类构造
	//析构顺序与构造相反
    example2 exp3("ljt","ljx");
    exp3.print();
}
int main()
{
    test01();
    return 0;
}

结果:

example1无参构造
example1 构造函数调用
example1 析构函数调用
example2构造函数调用
1内容ljx2内容ljt
example2析构函数调用
example1 析构函数调用

可见先调用example1,再调用example2,析构顺序相反。

静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化

静态成员函数:

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

有以下两个代码:

静态成员变量

//1-6-1 静态成员变量
#include<iostream>
using namespace std;
class example{
    public:
    static int a;//静态成员变量是共享的,类内声明,类外初始化,所有对象都可以访问这个数据
    private:
    static int b;//静态成员变量也是可以有访问权限的
};
int example::a = 10;
int example::b = 10;
int main()
{
    cout<<example::a<<endl;
    //第一种访问方式
    example::a = 60;
    cout<<example::a<<endl;
    //第二种
    example exp1;
    example exp2;
    exp1.a=20;
    cout<<"类"<<example::a<<endl;
    cout<<"exp1:"<<exp1.a<<endl;
    cout<<"exp2:"<<exp2.a<<endl;
    exp2.a =30;
    cout<<"类"<<example::a<<endl;
    cout<<"exp1:"<<exp1.a<<endl;
    cout<<"exp2:"<<exp2.a<<endl;
    //cout<<"b的值"<<endl;
    //cout<<example::b<<endl; 报错:成员 \"example::b\" (已声明 所在行数:11) 不可访问
    return 0;
}

运行结果

10
60
类20
exp1:20
exp2:20
类30
exp1:30
exp2:30

静态成员变量是共享的

静态成员函数

#include<iostream>
using namespace std;
class example
{

public:

	//静态成员函数特点:
	//1 程序共享一个函数
	//2 静态成员函数只能访问静态成员变量
	
	static void func()
	{
		cout << "func调用" << endl;
		a= 100;
		//b = 100; //错误,非静态成员引用必须与特定对象相对
	}

	static int a; //静态成员变量
	int b; // 
    private:

        //静态成员函数也是有访问权限的
    static void func2()
    {
        cout << "func2调用" << endl;
    }
};
int example::a = 10;


int main()
{
    //访问方式两种
    example::func();
    example exp1;
    exp1.func();
    //example::func2(); 函数 "example::func2" (已声明 所在行数:24) 不可访问
}

输出结果:

func调用
func调用

类和对象(C++对象模型和this指针,友元)

C++对象模型和this指针

在C++中,类内的成员变量成员函数分别存储,且只有非静态成员变量才会存储在类的对象上。

//2-7-1
#include<iostream>
using namespace std;
class example
{
    //example构造函数
    example()
    {
        exp1=0;
        cout<<"example构造函数调用"<<endl;
    }
    //非静态成员变量占对象空间
    int exp1;
    //静态成员变量不占对象空间
    static int exp2;
    //函数也不占对象空间,所有函数都是共享的。
    void func()
    {
        cout<<"func函数"<<endl;
    }
    //静态函数也是一样的
    static void func2()
    {
        cout<<"func函数"<<endl;
    }
};
int main()
{
    cout<<"类占空间"<<sizeof(example)<<endl;;
}

输出结果:

类占空间4

如果我们把其他都注释掉,只留下exp1会发生:

输出结果:

类占空间4

this指针

C++提供了一个特殊的指针使得这个指针指向被调用的成员所属的对象,就是this指针。

this指针是隐含的,是无需自行定义的。

用途:

  • 当形参和成员变量同名的时候用以区分,不过,这一点可以通过在命名规范来解决。(2-7-2)
  • 在类的非静态成员函数中返回对象本身,可使用return *this(2-7-3)
//2-7-2
#include<iostream>
using namespace std;
class example
{
    public:
    void func(int a)
    {
        //this指针区分同名成员变量和形参
        this->a=a;
    }

    int a;
};
int main()
{
    example ex;
    ex.func(10);
    cout<<ex.a<<endl;
}
//2-7-3
#include<iostream>
using namespace std;
class example
{
    public:
    int a;
    example& func(int a)
    {
        this->a=a;
        return *this;
    }
};
int main()
{
    example ex;
    ex.func(10).func(20);
    cout<<ex.a<<endl;
}

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针,否则会报错

    //2-7-4
    #include<iostream>
    using namespace std;
    class example
    {
        public:
        int a;
        example& func(int a)
        {
            this->a=a;
            return *this;
        }
    };
    int main()
    {
        example* ex=NULL;
        ex->func(10).func(20);
        cout<<ex->a<<endl;
    }

上面这串代码会报错,因为空指针中不能调用成员函数,但是如果把func函数中的this指针去掉就不会报错。

const修饰成员函数

常函数

成员函数后加const修饰就是常函数void example() const{},常函数的特点是不能修改成员变量,只能读取成员变量。不可修改成员属性。成员属性声明时加关键字mutable后,在常函数中依然可以修改。

常对象

声明对象前加const称该对象为常对象,常函数只能调用常函数,常对象只能调用常函数。

//2-7-5
#include<iostream>
using namespace std;
class example
{
    public:
    example()
    {
        a=0;
    }
    int a;
    example& func(int a)
    {
        this->a=a;
        return *this;
    }
    void print() const
    {
        cout<<a<<endl;
    }
    //void newfunc(int a) const
    //{
        //this->a=a;常函数不能修改成员变量
        //func(int a);常函数不能调用非常函数

    //}
};
int main()
{
    example ex;
    ex.func(10).func(20);
    ex.print();
    cout<<ex.a<<endl;
    const example ex2;
    //ex2.func(10);常对象只能调用常函数
    ex2.print();
}

友元

友元分为两种:全局函数做友元,类做友元。类做友元的时候,可以分为成员函数做友元,成员变量做友元。

在程序中,有时候需要访问类的私有成员,这时候就需要用到友元。友元的目的是为了访问类的私有成员。

以下是一个例子:

//2-8-1
#include<iostream>
using namespace std;
class example
{
    friend void goodprint(example &ex);
    public:
    example(int a,int b)
    {
        this->a=a;
        this->b=b;
    }

    private:
    int a;
    int b;
};
void goodprint(example &ex)
{
    cout<<ex.a<<endl;
    cout<<ex.b<<endl;
}
int main()
{
    example ex(10,20);
    goodprint(ex);
}

如果不加friend,就会因为权限问题,不可访问私有成员。

类做友元:

//2-8-2
#include<iostream>
using namespace std;
class example
{
    friend class example2;
    public:
    example(int a,int b)
    {
        this->a=a;
        this->b=b;
    }

    private:
    int a;
    int b;
};
class example2
{
    public:
    void print(example *ex)
    {
        cout<<ex->a<<endl;
        cout<<ex->b<<endl;
    }
};
int main()
{
    example ex(10,20);
    example2 ex2;
    ex2.print(&ex);
}

成员函数做友元:

//2-8-3
#include<iostream>
using namespace std;
class example2;
class example
{
    public:
    example();
    void print();
    private:
	example2 *ex;
};
class example2
{
    public:
    friend void example::print();
    example2()
    {
        a = 10;
        b = 20;
    }
    example2(int a, int b)
    {
        this->a = a;
        this->b = b;
    }
    private:
    int a;
    int b;
};
example::example()
{
    ex = new example2;
}
void example::print()
    {
        cout<<ex->a<<endl;
        cout<<ex->b<<endl;
    }
int main()
{
    example2 ex(100,20);
    example ex2;
    ex2.print();
    return 0;
}

类和对象(继承)

继承

C++面向对象具有三大特点:封装、继承、多态。

不难发现的是,有些类的成员变量和成员函数与其他类的成员变量和成员函数有很多相似的地方,这时候就可以使用继承来减少代码量。

例如,马和驴同属于动物,他们呢又可以生出来骡子,具有马和驴的属性。

继承的基本写法:

class 子类名:继承方式 父类名
{
    //子类的成员变量和成员函数
}

继承方式有三种:
私有继承,公共继承和保护继承。

分为以下三种情况(内容代表在子类中的访问权限):

继承方式父类的private父类的public父类的protected
私有继承不可访问privateprivate
公共继承不可访问publicprotected
保护继承不可访问protectedprotected

注意:父类中的private成员变量和成员函数在子类中都是不可访问的。但是private会被继承下去

私有继承

私有继承的写法:

class 子类名:private 父类名
{
    //子类的成员变量和成员函数
}

私有继承的特点是:子类中的成员变量和成员函数不能直接访问父类的成员变量和成员函数,但是可以通过父类的公共成员函数来访问。

例如:

//3-9
#include<iostream>
using namespace std;
class example1
{
    public:
    int a=1;
    protected:
    int b=2;
    private:
    int c=3;
};
class example2 :private example1
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
int  main()
{
 example2 ex;
 //cout<<ex.a<<endl;这显然是不可以的,因为私有继承,这个访问权限是private的
 //cout<<ex.b<<endl;同理,这个访问权限也是private的
  ex.print();
}

所以当私有继承的时候,子类中的成员函数不能直接访问父类的成员变量和成员函数,但是可以通过父类的公共成员函数来访问。

公共继承

公共继承的写法:

class 子类名:public 父类名
{
    //子类的成员变量和成员函数
}

公共继承的特点是:子类中的成员变量和成员函数可以直接访问父类的成员变量和成员函数。

就不举例子了,和父类的所有访问权限都相同(仅限于public和protected)

保护继承

保护继承的写法:

class 子类名:protected 父类名
{
    //子类的成员变量和成员函数
}

保护继承的特点是:子类中的成员变量和成员函数可以直接访问父类的成员变量和成员函数,但是不能通过子类的对象来访问。

例如:

//3-10
#include<iostream>
using namespace std;
class example1
{
    public:
    int a=1;
    protected:
    int b=2;
    private:
    int c=3;
};
class example2 :protected example1
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
class example3 :protected example2
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
int  main()
{
 example2 ex;
 //cout<<ex.a<<endl;//这显然是不可以的,因为私有继承,这个访问权限是protected的
 //cout<<ex.b<<endl;//同理,这个访问权限也是protected的
 //如example3 所示,protected继承,子类中的成员函数可以访问父类中的protected成员变量和成员函数
  ex.print();
    example3 ex2;
    ex2.print();//example3也可以访问到a,b说明protected继承的子类访问权限是protected
}

注意:private权限虽然不可访问,但是会被继承下去。

可以采用visual studio的命令行参数来查看类的内存布局,例如:

cl /d1 reportSingleClassLayout类名 文件名

继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

同名处理:

  • 子类对象可以直接访问到子类中同名成员
  • 子类对象加作用域可以访问到父类同名成员
  • 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
  • 静态成员和非静态成员出现同名,处理方式一致。
  • 规范命名。

多继承

多继承的写法:

class 子类名:继承方式1 父类名1,继承方式2 父类名2
{
    //子类的成员变量和成员函数
}

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

虚继承

虚继承的写法:

class 子类名:virtual 继承方式 父类名
{
    //子类的成员变量和成员函数
}

虚继承的特点是:解决多继承中父类中有同名成员出现的问题。

例如:

//3-11
#include<iostream>
using namespace std;
class example1
{
    public:
    int a=1;
    protected:
    int b=2;
    private:
    int c=3;
};
class example2 :virtual public example1
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
class example3 :virtual public example1
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
class example4 :public example2,public example3
{
    public:
    void print()
    {
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;//父类中的private成员变量和成员函数在子类中都是不可访问的
    }
};
int  main()
{
 example4 ex;
  ex.print();
}

可以看到,example4中的a,b都是1,2,说明虚继承解决了多继承中父类中有同名成员出现的问题。

虚继承可以解决菱形继承的问题,菱形继承的代码如下:

#include<iostream>
using namespace std;
class example
{
public:
    int m_Age=100;
};
class example2 :virtual public example
{
public: 
    int m_Age=102;
};
class example3:virtual public example
{
    int m_age=103;
};
class example4:virtual public example2,virtual public example3
{
    public:
    void print()
    {
        cout<<example::m_Age<<endl;
        cout<<example2::m_Age<<endl;
        cout<<example3::m_Age<<endl;
        cout<<example4::m_Age<<endl;
    }
};
int main()
{
    example4 e;
    e.print();
    return 0;
}
100
102
100
102

类和对象(多态)

多态

多条是C++面向对象的三大特性之一

多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指针或引用调用成员函数,执行不同的函数。

多态的分类

多态分为两种:编译时多态和运行时多态。

  • 编译时多态:函数重载和运算符重载属于编译时多态,因为函数重载和运算符重载是静态的,函数地址早绑定。

  • 运行时多态:虚函数和抽象类属于运行时多态,因为虚函数和抽象类是动态的,函数地址晚绑定。

案例:

//多态-1
#include <iostream>
using namespace std;
class Base
{
public:
     void func()
     {
        cout<<"Base func"<<endl;
     }
};
class Derived : public Base
{
    void func()
    {
        cout << "Derived func()" << endl;
    }
};
int main()
{
    Base *p = new Derived();
    p->func();
    return 0;
}

运行结果:

Base func()

说明并未发生重写,而是发生了隐藏,因为在编译时,编译器会根据指针的类型来决定调用哪个函数,而不是根据指针所指向的对象的类型来决定调用哪个函数。

但是,如果将函数声明为虚函数,就可以实现运行时多态,如下所示:

//多态-2
#include <iostream>
using namespace std;
class Base
{
public:
    virtual void func()
    {
        cout << "Base func()" << endl;
    }
};
class Derived : public Base
{
    public:
    void func()
    {
        cout << "Derived func()" << endl;
    }
};
int main()
{
    Base *q= new Base();
    Base *p = new Derived();
    Derived *d = new Derived();
    q->func();
    p->func();
    d->func();
    return 0;
}
Base func()
Derived func()
Derived func()

显然的,这里的多态是运行时多态,因为在运行时,编译器会根据指针所指向的对象的类型来决定调用哪个函数。

总结:

多态满足条件

  • 有继承关系

  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

相关概念:重写:函数返回值类型 函数名 参数列表 完全一致称为重写

多态的实现

多态的实现有两种方式:虚函数和抽象类。

  • 虚函数:虚函数是在基类中使用关键字virtual声明的函数,虚函数的特点是在运行时才动态绑定,可以实现运行时多态。

  • 抽象类:抽象类是包含纯虚函数的类,抽象类的特点是不能实例化对象,只能作为接口使用。

多态的好处

多态的好处是提高了代码的扩展性和复用性。

  • 提高了代码的扩展性:通过多态,可以在不修改原有代码的基础上,对原有代码进行扩展,增加新的功能。

  • 提高了代码的复用性:通过多态,可以在不修改原有代码的基础上,对原有代码进行复用,增加新的功能。

多态的弊端

多态的弊端是降低了代码的可读性和可维护性。

  • 降低了代码的可读性:通过多态,可以在不修改原有代码的基础上,对原有代码进行扩展,增加新的功能。

  • 降低了代码的可维护性:通过多态,可以在不修改原有代码的基础上,对原有代码进行复用,增加新的功能。

虚函数

虚函数是在基类中使用关键字virtual声明的函数,虚函数的特点是在运行时才动态绑定,可以实现运行时多态。

虚函数的声明

虚函数的声明格式如下:

virtual 返回值类型 函数名(参数列表);

纯虚函数和抽象类

纯虚函数是在基类中使用关键字virtual声明的函数,并在函数后面加上=0,纯虚函数的特点是没有函数体,只有函数原型,不能被调用,只能被重写。

抽象类是包含纯虚函数的类,抽象类的特点是不能实例化对象,只能作为接口使用。

纯虚函数和抽象类的声明格式如下:

class 类名
{
public:
    virtual 返回值类型 函数名(参数列表) = 0;
};

只要有纯虚函数的类,就是抽象类,抽象类不能实例化对象,只能作为接口使用。

使用时注意:

  • 抽象类无法实例化对象
  • 子类必须重写父类中的纯虚函数,否则也属于抽象类

示例:

//多态-3
#include <iostream>
using namespace std;
class Base
{
public:
    virtual void func() = 0;
};
class Derived : public Base
{
    public:
    void func()
    {
        cout << "Derived func()" << endl;
    }
};
class Derived2 : public Base
{
    public:
    void func()
    {
        cout << "Derived2 func()" << endl;
    }
};
int main()
{
    Base *p = new Derived();
    Derived2 d2;
    p->func();
    d2.func();
    return 0;
}
Derived func()
Derived2 func()

子类重写父类中的虚函数,就可以实现运行时多态。

虚析构函数和纯虚析构函数

虚析构函数是在基类中使用关键字virtual声明的析构函数,虚析构函数的特点是在运行时才动态绑定,可以实现运行时多态。

纯虚析构函数是在基类中使用关键字virtual声明的析构函数,并在函数后面加上=0,纯虚析构函数的特点是没有函数体,只有函数原型,不能被调用,只能被重写。

虚析构函数和纯虚析构函数的声明格式如下:

virtual ~类名();
virtual ~类名() = 0;

使用纯虚析构的好处是可以防止内存泄漏,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。所以需要虚析构函数或者纯虚析构函数。

使用纯虚析构函数的时候同样不能实例化对象,只能作为接口使用。

虚析构示例:

//多态-4
#include <iostream>
using namespace std;
class Base
{
public:
    virtual ~Base()
    {
        cout << "Base析构函数" << endl;
    }
};
class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "Derived析构函数" << endl;
    }
};

void test01()
{
    Base *p = new Derived();
    delete p;
}
int main()
{
    test01();
    return 0;
}
Derived析构函数
Base析构函数

纯虚析构示例:

//多态-5
#include <iostream>
using namespace std;
class Base
{
public:
    virtual ~Base() = 0;
};
Base::~Base()
{
    cout << "Base析构函数" << endl;
}
class Derived : public Base
{
public:
    int *p;
    Derived()
    {
        p = new int(10);
    }
    ~Derived()
    {
        cout << "Derived析构函数" << endl;
        //释放堆区数据
        if (p != NULL)
        {
            delete p;
            p = NULL;
        }
    }
};

void test01()
{
    Base *p = new Derived();
    delete p;
}
int main()
{
    test01();
    return 0;
}
Derived析构函数
Base析构函数
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

美丽新科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值