CPP类与对象总结(详细易懂)

Cpp中的类与对象总结。

很多人对C语言与cpp语言的区别的解释为:c语言是面向过程的,而c++是面向对象的。当然也有大佬反对这句话,但不管怎样,都可以看出面向对象是c++的一大重要特征,甚至很多c++课程将面向对象的课程称之为“c++”核心编程。关于这一方面的总结csdn上也有很多,我相信我的总结并不会比他们详细。但我希望以一个学习者的角度来解释这一看似复杂高深的内容,可以让大家获益。很多内容均来自个人的学习笔记与课程笔记。

在这里插入图片描述

类与对象概述:

提到类,首先想到“class”,class作为类的声明,有类就有它。然后就是“属性”与“方法”,这二者几乎构成了类的主题。简单一点理解,属性相当于名词,而方法相当于动词,当然详情请看下文。接下来可能就是“权限”了,涉及到类外访问,及子类访问的问题。再然后就是**“构造函数” “析构函数”**,这涉及初始化,深浅拷贝及清空堆区等问题。也是比较复杂的一块内容。最后就是所有面向对象的编程语言都具有的三块“封装”、“继承”与“多态”。

下面我们来一一分析一波。

类的基本实现方法:

请看代码中的注释:(public可以先不用管,详见下文权限内容)

#include<iostream>
using namespace std;

class zan {
public:
    //定义字符串,zan类下的观众属性,为字符串赋值
    string viewer = "读者老爷";
    //定义字符串,zan类下的点赞属性,为字符串赋值
    string dianZan = "请点一个宝贵的赞";

    //定义thanks函数进行输出操作
    void thanks() {
        cout << viewer << "," << dianZan << endl;
    }
    };
    //测试函数,实例化对象并调用thanks方法,等……
    void thank() {
        zan your_zan;
        your_zan.thanks();
        cout << "谢谢浏览。" << endl;
    }
    //在主函数中调用函数    
    int main() {
        //^_^//
        thank();
        system("pause");
        return 0;
    }

好吧,我承认上面的代码很皮,但大家可以通过它具体了解一下cpp中对对象相关操作的基本内容。一个类的实现,可以说基本上就是这样的了。

权限

权限总共有三种,上文大家看到的“public”就是一种,还有“private”和“protected”,权限的字面意思应该很好理解,如果我们把这篇文章定义为一个类,这篇文章发表了,所有人都可以看,这就是public。如果这篇文章没有发布,储存在我这台带有开机密码的电脑里,那么这就是private。protected与private相似,唯一的区别就是其继承的子类(下文将具体讲解)是否可以访问的问题,private子类不可访问,protected子类可以访问。在类中若无权限声明,将默认为private。(注:结构体的权限默认为公开)。所有上面的代码中如果没有public,在下文的thanks函数中便不可以访问zan之中的thank方法。也许在一开始我们认为一个类中全部使用public方法可能是最方便的了。但其实,在真正的开发中,我们一般将属性的权限都设为private。其实,这也不难理解,一旦超越权限,后果将会不堪设想。。

//输入两个立方体的各边,判断是否为同一立方体。利用表面积与体积相等判断。
#include<iostream>
using namespace std;
class Cube
{
//公共权限下的 设置长宽高 与 获取长宽高 的方法。
//注意长宽高在下面是私有属性,在类外访问不到。
public:
    void setl(int l) {
        m_l = l;
    }
    int getl() {
        return m_l;
    }
    void setw(int w) {
        m_w = w;
    }
    int getw() {
        return m_w;
    }
    void seth(int h) {
        m_h = h;
    }
    int geth() {
        return m_h;
    }
    //以引用的形式传入另一个cube类,并判断两边是否相等。
    bool adj(Cube &a) {
        if (m_l == a.m_l&&m_w == a.m_w&&m_h==a.m_h) {
            return true;
        }
        else
        {
            return false;
        }
    }
    //下面是上文使用过的私有属性,长宽高。
private:
    int m_l;
    int m_w;
    int m_h;
};
//这个与上面的类中的bool adj功能相同。(两种不一样的方法)
bool adjust(Cube &a,Cube &b) 
{
    if (a.getl() == b.getl() && a.getw() == b.getw() && a.geth() == b.geth())
    {
        return true;
    }
    else
    {
        return false;
    }
}
int main() {
    //类的实例化,及参数赋值。
    Cube c1;
    c1.setl(10);
    c1.setw(10);
    c1.seth(10);
    Cube c2;
    c2.setl(10);
    c2.setw(10);
    c2.seth(10);
    int l = c1.getl();
    int w = c1.getw();
    int h = c1.geth();
    cout << "表面积为: " << 2 * l * w + 2 * l * h + 2 * w * h << endl;
    cout << "体积为: " << l * h * w << endl;
    bool ret = adjust(c1,c2);
    if (ret)
    {
        cout << "二者为同一立方体" << endl;
    }
    else
    {
        cout << "二者不为同一立方体" << endl;
    }
    bool ret1=c1.adj(c2);
    if (ret1)
    {
        cout << "在对象函数中输出,二者为同一立方体" << endl;
    }
    else
    {
        cout << "在对象函数中输出,二者不为同一立方体" << endl;
    }
    system("pause");
    return 0;
}

上面的代码,主要是为了方便大家理解权限,代码过于冗长(主要也是为了进一步加深大家对类的理解,特别是上文bool adjust 在类内与内外的不同实现方法)。我们看到了属性与方法的不同权限,这也是为了规范,即在类外我们一般不可以直接动属性,而必须通过public下的特定方法来实现。这样在上千行代码中你便少了一种出现bug的可能,你也失去了一次秃头的机会。

我们还是就上面这一段代码本身来讲解一下吧。在函数传入的参数中,我们可以传入一个类。因为是形参,所有我们一般以引用的方式传入(关于形参与引用可以自行csdn)。这个函数可以在类内也可以实现,类外也可以实现,但传入的参数是有区别的。这也很好理解。因为类内的函数本来就可以访问这个类中的所有属性与方法。

默认函数

  1. 创建一个类,c++编译器会给每个类都添加至少三个函数
  • 默认构造(空实现)
  • 析构函数(空实现)
  • 拷贝函数(值拷贝)
  1. 如果我们写了有参构造函数,编译器不会再提供默认构造,但依然提供拷贝构造。
  2. 如果提供了拷贝构造函数,编译器将不再添加其他函数。

构造函数

构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

构造函数语法(初始化)

构造函数语法:类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用而且只套调用一次

构造函数的分类及调用两种分类方式:

  1. 按参数分为:有参构造,无参构造(默认)。

  2. 按类型分为:普通构造,拷贝构造。

  3. 三种调用方式:括号法,显示法,隐式转换法。

//在person类中
	//一般我们用拷贝函数实现初始化
person(int a){}//有参构造
person(){}//无参构造
person(const person &p){}//拷贝构造

/*++++++++++++++调用构造函数++++++++++++++++++*/
			//与实例化一起进行

//===括号法===
    person p1;//默认构造
    //person p1();不正确,编译器会认为是声明
    person p2(10);//有参构造
    person p3(p2);//拷贝构造
//===显示法===
    person p1;//默认构造
    person p2 = person(10);//有参构造
        person(10);//匿名对象,执行完后立即释放,因为并没有实例来接收,一般不建议使用。 
    person p3 = person(p2);//拷贝构造
    //注意不要利用拷贝构造函数初始化匿名对象
    	/*person(p3);编译器会认为重定义
    	因为系统认为person(p3)==person p3*/
//===隐式转换法===
	person p4 = 10;/*相当于*/person p4 = person(10);
	person p5 = p4;//拷贝构造

上面的内容还是很详细的,其实我们只要多练习,并能熟悉自己常用的就可以了。我们真正使用构造函数主要还是为了初始化某些属性,已经让某些功能更好地实现。

构造函数还可以实现初始化列表:如下

//法1
class Person
{
    Person():m_A(10),m_B(20),m_C(30)
    {
        
    }
    int m_A;
    int m_B;
    int m_C;
};
//法2
class Person
{
    Person(int a,int b,int c) :m_A(a),m_B(b),m_C(c)
    {
        
    }
    int m_A;
    int m_B;
    int m_C;
};

析构函数

​ 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

析构函数语法(清理)

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

1.析构函数,没有返回值也不写void
2.函数名称与类名相同在名称前加上符号 ~
3.析构函数不可以有参数,因此不可以发生重载
4.程序在对象销毁前会自动调用析构,无须手动调用而且只会调用一次

析构函数的实现基本上与构造函数相似,但不能传入参数,所有更简洁吧。这里不再过多示例,

~person(){ /*注意这是person类中的一个析构函数*/}

这些函数执行的顺序,大家可以在构造函数与析构函数中加入 cout<<""<<endl; 语句自行验证其执行顺序(一般来说构造函数最先调用,析构函数最后调用)还是加入几行代码演示一下吧,大家可自行运行一下。

//下面代码在一个类中实例化了另一个类
#include<iostream>
using namespace std;
#include<string>
class Phone {
public:
	Phone(string Name) 
	{ 
		m_PName = Name;
		cout << "phone的构造函数调用" << endl;
	}
	string m_PName;
	~Phone() {
		cout << "phone的析构函数调用" << endl;
	}
};
class Person {
public:
	Person(string Name, string PName) :m_Name(Name), m_Phone(PName)
	{
		cout << "person的构造函数的调用" << endl;
	}
	string m_Name;
	Phone m_Phone;
	~Person() {
		cout << "person的析构函数调用" << endl;
	}
};
void test01() {
	Person p1("zhangsan", "huawei mate 30");
	cout << p1.m_Name << "拿着" << p1.m_Phone.m_PName << "手机" << endl;
}

int main() {
	test01();
	system("pause");
	return 0;
}

拷贝函数

拷贝函数使用时机:

  • 使用一个已经创建完成的对象来初始化一个新的对象
  • 值传递的方式给函数传值。
//值传递
void doWork(Person p)
{
    
}
void testo2(){
    Person p;//实例化,默认构造函数
    //调用上面的函数并传入这个p
    doWork(p);//拷贝构造函数
}

//值传递本身也就是拷贝一份新的数据到栈区

我们简单来分析一下,类的值传递和传入一个 int a 是一样的。关键是我们要理解“值传递本身也就是拷贝一份新的数据到栈区”这句话。(关于栈区与堆区详解可以自行csdn)。

值方式返回局部对象。

Person doWork()
{
    person p1;//局部变量函数结束即释放
    return p1;//返回的其实是拷贝
    /*会调用拷贝构造函数*/
}
void test(){
    Person p = doWork()//
}

大家可以在编译器中自行实现一下这串代码,相信可以加深大家对拷贝函数的理解。

深拷贝与浅拷贝

注意:!!!(难点与重点)

如果利用编辑器提供的函数进行拷贝会进行浅拷贝的操作。

浅拷贝:逐字节拷贝(即完全复制)。

若拷贝的对像在堆区开辟了一个指针"new int(shuju)",那么在析构函数中进行delete销毁操作(堆区开辟的数据由程序员自行销毁)也会进行两次,而第二次则是非法的,会导致程序崩。

可以用深拷贝的方式解决。重新申请内存空间

即不利用自行创建的函数,而是自行创建一个拷贝函数。

class Person
{
	Person(const Person &p){
        //m_Height = p.m_Height,默认拷贝函数自行创建的过程
        m_Height = new int(*p.m_Height);
        //重新开辟一块新的内存,可以解决上述非法操作。
    }
    ~Person()
    {	
        //有关指针代码的规范,防止野指针与空指针。
        if(m_Height != NULL)
    	{
        delete m_Height;
        m_Height = NULL;
    	}
    }
};

关于这几个函数我就介绍这么多吧。当然还有一些内容我没有讲到,也希望大家可以自己积极去了解。

this指针

具体在代码中解释吧。

#include<iostream>
using namespace std;
class Person {
public:
	Person(int age) 
	{	
		//this指针可解决形参与成员变量同名的问题。
		//this指针指向被调用的成员函数所属的对象。
		//建议正确使用命名规范。
		this->age = age;
	}
	Person& Person_age(Person p) {
		this->age += p.age;
		//返回对象本身。
		return *this;
	}
	//需要用引用的方式返回,否则会拷贝一份新的数据,无法实现连环“.”(下面的链式编程思想)。
	//引用则会一直赋值在该对象上。
	int age;
};
void test01() {
	Person p1(10);
	Person p2(10);
	//链式编程思想。
	p2.Person_age(p1).Person_age(p1).Person_age(p1);
	cout << "p1的年龄为" << p1.age << endl;
	cout << "p2的年龄为" << p2.age << endl;

}


int main() {
	test01();
	system("pause");
	return 0;
}

其实,简单点理解this指针就是“我自己”。有了这样的一种表述方法,我们就可以更方便地与编译器对话了。

封装 继承 多态

其实创建一个类本身就是一个封装的过程。所以这里我们就主要就继承与多态讲解。

继承:

“继承”正如字面意思,就是子类从父类那里继承父类的相关属性与方法(实际上继承了父类的全部内容,只不过存在权限允不允许访问的问题)

class IntelCpu :public:: CPU{}//即Intelcpu继承了父类cpu的内容。

继承时在 冒号后面会有“public 等,这涉及到继承权限的问题

1) public继承方式(不变)

  • 基类中所有public成员在派生类中为public属性;
  • 基类中所有protected成员在派生类中为protected属性;
  • 基类中所有private成员在派生类中不可访问。

2) protected继承方式(public变为protected)

  • 基类中的所有public成员在派生类中为protected属性;
  • 基类中的所有protected成员在派生类中为protected属性;
  • 基类中的所有private成员在派生类中仍然不可访问。

3) private继承方式(public变为private)

  • 基类中的所有public成员在派生类中均为private属性;
  • 基类中的所有protected成员在派生类中均为private属性;
  • 基类中的所有private成员在派生类中均不可访问。

多态:

C++的多态性主要是重载虚函数。(也就是静态多态性和动态多态性)

函数重载:即同一个名称但因为传入参数不同而可以分别实现不同功能。

#include<iostream>
using namespace std;

int zan(int x,int y)  //进行加法运算
{
    return x+y;
}

float zan(float x,float y)  //进行乘法运算
{
    return x*y;
}

int main()
{
    int a=1;
    int b=2;
    float c=1;
    float d=2;
    cout<<zan(a,b)<<endl;
    cout<<zan(c,d)<<endl;
    return 0;  
}

注意:::无法区分仅按照返回值区分的重载!!!**

运算符重载:两个类之间直接进行加号运算是无意义的。但我们可以(借助运算符重载)让其有意义。

用 operator"运算符" 来命名函数,可以是成员函数或全局函数。

//  <>不需要写出
<返回类型说明符> operator <运算符符号>(<参数表>)
{
 
     <函数体>
 
}

进行了运算符重载,你就可以通过运算符实现你所需要的功能,例如class person 有两个属性身高体重,你可以重载+运算符,从而实现通过类相加,让其两个属性都相加。但注意你的重载必须要方便理解,且有效。

注意:不会利用成员函数重载<<运算符,因为无法实现cout在左侧,只能用全局函数重载左移运算符。

因为使用成员函数只会这样:P1 << cout; 显然不符合代码的常理。

//ostream为cout对应的类型
ostream operator<<(ostream &cout,Person &p){
    return cout;
}
//使其可以符合链式思想(即一直追加<< )。

虚函数:这里不再具体展开了,因为字数已经够多了。推荐查看c++虚函数详细讲解。简单一点理解,就是子类在继承父类时又想在父类基础上改进,于是发生重载,让自己改进的方法覆盖父类的方法。

对于c++类与对象希望大家可以系统深入地学习。因为上文对类与对象的讲解并不完整。友元,静态成员等等都没有讲解到,还有很多规范也没有涉及。但我希望这篇文章会对大家或多或少有所帮助,谢谢了。

声明:转载请注明出处。码龄不够,可能出现错误,欢迎批评指正。

尽管c++有很多人喷它,但却也有很多人爱它。无论如何,作为学习者而言我们都应积极应对它。因为努力与付出终会有回报。我们无数次尝试改变的可能,我们便拥有了无数次提升自我的机会,而未来也因为我们,有了更多发展的可能。加油,码农!

posted @ 2022-02-05 16:37  litecdows  阅读(303)  评论(0编辑  收藏  举报