写在前面

写本系列的目的(自用)是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。以前三天打鱼两天晒网地学习C++,一直无法对C++熟练掌握,核心的面向对象编程并不熟悉,STL语法只会部分,因此也希望自己未来能做到熟悉甚至精通C++。

Part 2 面向对象编程

该部分主要通过b站黑马程序员的视频来进行学习,记录笔记。全部是概念性的东西并不能帮助理解知识,直接上代码并以注释的形式讲解会更易于理解。

二、类和对象

2、对象特性

C++对象特性这部分知识杂且多,不可能马上就能记住并理解。多写代码,在实际操作过程中遇到问题及时复习,才能真正掌握。

(3)拷贝构造函数的调用时机

C++中拷贝构造函数调用时机通常有三种情况

1、使用一个已经创建完毕的对象来初始化一个新对象,这个在上一节也有说明。

2、值传递的方式给函数参数传值。值传递相当于拷贝了一份副本传入函数。

3、以值方式返回局部对象。返回对象时,也是新创建了一个变量,并将值拷贝给它。

#include<iostream>
using namespace std;

class Person
{
private:
    int m_Age;
public:
    Person()
    {
        cout<<"默认构造函数"<<endl;
    }
  
    Person(int age)
    {
        m_Age = age;
        cout<<"有参构造函数"<<endl;
    }
    
    Person(const Person &p)
    {
        cout<<"拷贝构造函数"<<endl;
        m_Age = p.m_Age;
    }

    ~Person()
    {
        cout<<"析构函数构造"<<endl;
    }
};

void test1()
{
    Person p1(20);           //这一行会调用有参构造函数
    Person p2(p1);           //使用已经存在的对象初始化一个对象,调用拷贝构造函数
}

void doWork(Person p){  }    //值传递相当于拷贝了一份原来p3的临时副本,所以是拷贝构造函数

void test2()
{
    Person p3;               //这一行会调用默认构造函数
    doWork(p3);              //值传递的方式给函数参数传值,这一行会调用拷贝构造函数
} 

Person doWork2()
{
    Person p4;               //这一行对调用默认构造函数
    return p4;               //以返回值的方式传值,相当于创建了一个对象,将p4拷贝过去后返回
}                            //所以会调用拷贝构造函数

void test3()
{
    Person p5 = doWork2();
}                          

int main()
{
    test1();                 //结束后调用两次析构函数
    cout<<"-----------------"<<endl;

    test2();                 //结束后调用两次析构函数
    cout<<"-----------------"<<endl;

    test3();                 //结束后调用两次析构函数
    cout<<"-----------------"<<endl;

    system("pause");
    return 0;
}
(4)构造函数的调用规则

默认情况下,c++编译器至少给一个类添加3个函数,用户不写,那么编译器就会给你写。:

默认构造(空实现)、析构函数(空实现)、拷贝构造(值拷贝)。

如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造。这时候假如用户也没用写默认函数,那么可能出现问题。这时候如果你写Person p,采用默认构造,那么就会出错,因为c++编译器也没有给你写默认构造函数,编译器没有默认构造函数调用了。

如果用户定义拷贝构造函数,c++不会再提供其他构造函数。这时候假如用户也没用写另外两个构造函数,那么在写Person p和Person(10)时都会报错,因为编译器没有默认构造函数和有参构造函数调用了。

(5)深拷贝与浅拷贝

浅拷贝:常见的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

编译器给我们写的拷贝构造函数是浅拷贝,平常并不会出现问题,但在需要申请空间的操作时,配合上析构函数会出现问题。

先补充一下new和delete的用法:在C++中,new和delete是用于动态内存管理的运算符,它们允许在运行时分配和释放内存。用户手动分配和释放的内存都在堆上。

//动态分配单个对象
int* ptr = new int;       // 分配一个int大小的内存,值未初始化
*ptr = 42;                // 赋值

int* ptr2 = new int(10);  // 分配并初始化为10

delete ptr;               // 释放内存
delete ptr2;

//动态分配数组
int* arr = new int[5];    // 分配包含5个int的数组,值未初始化

// 初始化数组元素
for (int i = 0; i < 5; ++i) {
    arr[i] = i * 2;
}

delete[] arr;             // 释放数组内存(必须用[])

 需要注意的几个点是:

1、new申请内存后,要记得调用delete清楚,否则可能导致内存泄漏

2、不能对同一块空间重复释放

3、不能释放非new分配的内存

下面来看示例:

#include<iostream>
using namespace std;

class Person
{
private:
    int m_Age;                     
    int *m_Height;               //指针,指向用户身高所在的地址

public:
    Person()
    {
        cout<<"默认构造函数"<<endl;
    }

    Person(int age, int height)
    {
        m_Age = age;
        m_Height = new int(height);   //这里动态申请了一块int大小的地址,值为用户传入的height
        cout<<"有参构造函数"<<endl;
    }


    ~Person()
    {
        if(m_Height!=nullptr)
            delete m_Height;          //析构函数的作用于此体现,进行后处理,释放占用空间
        cout<<"析构函数"<<endl;
    }
};

void test()
{
    Person p1(18, 160);               //进行有参构造
    Person p2(p1);                    //系统为我们写好了拷贝构造函数,此处是拷贝构造
    cout<<"p1的年龄为:"<<p1.m_Age<<"身高:"<<*p1.m_Height<<endl;        //输出
    cout<<"p2的年龄为:"<<p2.m_Age<<"身高:"<<*p2.m_Height<<endl;
}

int main()
{
    test();

    system("pause");
    return 0;
}

以上代码在运行时系统会出错。出错原因就在拷贝函数身上。系统为我们写的拷贝函数如下:

Person(const Person &p)
{  
    m_Age = p.m_Age;
    m_Height = p.m_Height;
}

利用这个拷贝函数,是将原本p1的内容逐字节拷贝过来,如下图所示:

如图所示,p2的指针和p1的指针中保存的是同一个地址。那么在调用析构函数时,p1和p2会对同一块地址空间执行delete操作。我们知道,不能对同一块空间重复释放,因此系统就报错了。解决方式就是进行深拷贝,我们需要自己书写拷贝构造函数,如下:

Person(const Person &p)
{
    m_Age = p.m_Age;
    m_Height = new int(*p.m_Height);
}

这样,p2中的指针指向的就是另一块地址空间,只不过这块地址存储的值也是160。

(6)初始化列表

在传统方法中,我们采用构造函数的有参构造函数来为属性赋初值,这里介绍另外一种方法:初始化列表。

初始化列表的语法很简单,构造函数():属性1(值1),属性2(值2)...,如下:

Person():m_A(10),m_B(20),m_C(30)
{

}

 显然,这样赋初值的话不够灵活,初值永远不会改变。可以采用下例灵活地赋初值。

#include<iostream>
using namespace std;

class Person
{
public:
    int m_A;
    int m_B;
    int m_C;
  
    Person(int a, int b, int c): m_A(a), m_B(b), m_C(c)     //初始化列表初始化属性
    {
    
    }
};

void test()
{
    Person p(30,20,10);
    cout<<"m_A="<<p.m_A<<endl;
    cout<<"m_B="<<p.m_B<<endl;
    cout<<"m_C="<<p.m_C<<endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}
(7)类对象作为类成员

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

class A{   }

class B
{
    A a;
    ....
}

如上例所示,B类中有对象A作为成员。需要注意的是:

构造顺序为,先调用A的构造函数,再调用B的构造函数。可以理解为先做好部分,才能有整体。

析构顺序为,先调用B的析构函数,再调用A的析构函数。析构与构造的顺序反过来,可以按栈先进后出的顺序来理解。

(8)静态成员

静态成员就是在成员变量和成员函数前加上关键字static。主要有两种静态成员,静态成员变量和静态成员函数。

首先来说静态成员变量,满足三个特点:

1、所有对象共享同一份数据。无论谁修改了静态成员变量,每个成员的静态成员变量都会改变。所以严格来讲,静态成员变量不属于任何一个对象。因此,静态成员变量有两种访问方式,通过对象访问和通过类名访问。

2、在编译阶段分配内存。

3、类内声明,类外初始化。

#include<iostream>
using namespace std;

class Person
{
public:

	static int m_A;         //静态成员变量,类内声明

private:
	static int m_B;         //静态成员变量也是有访问权限的
};


int Person::m_A = 10;       //类外初始化   
int Person::m_B = 10;       //注意,虽然是私有的,但是可以初始化。除了初始化的行为都不允许

void test01()
{
	//静态成员变量两种访问方式
	Person p1;                  //通过对象
	p1.m_A = 100;               //注意,这里所有对象的m_A都变为了100
	cout << "p1.m_A = " << p1.m_A << endl;

	Person p2;
	p2.m_A = 200;               //注意,这里所有对象的m_A都变为了200
	cout << "p1.m_A = " << p1.m_A << endl; //由于共享同一份数据,所以都是200
	cout << "p2.m_A = " << p2.m_A << endl;

	cout << "m_A = " << Person::m_A << endl;   //通过类名


	//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}

int main() {

	test01();

	system("pause");

	return 0;
}

接下来说静态成员函数,满足以下特点:

1、所有对象共享同一个函数。所以严格来讲,静态成员函数不属于任何一个对象。因此,静态成员函数有两种访问方式,通过对象访问和通过类名访问。

2、静态成员函数只能访问静态成员变量。因为静态成员函数如果访问了非静态成员变量,编译器会不知道它访问的是哪一个成员的非静态成员变量。

#include<iostream>
using namespace std;

class Person
{
public:

    static int m_A;   //静态成员变量
    int m_B;          //非静态成员变量

    static void func()
    {
        m_A = 100;    //静态成员函数可以访问静态成员变量
        //m_B = 200;  //这句解开注释会报错,因为静态成员函数不能访问非静态成员变量
        cout<<"static void func"<<endl;
    }

private:
    static void func2()     //私有的静态成员函数
    {
        cout<<"static void func2"<<endl;
    }
};

int Person::m_A = 0;    //在类外进行初始化

void test()
{
    Preson p;           //通过对象访问静态成员函数
    p.func();

    Person::func();     //通过类名访问静态成员函数
    //Person::func2();    //如果解开注释会报错,因为类外访问不到私有的静态成员函数
} 

int main()
{
    test();
    system("pause");
    return 0;
}
(9)C++对象模型和this指针

C++的成员变量和成员函数是分开存储的。

如果类为空,那么这个类占的空间大小为1个字节;如果类中仅包含一个非静态成员(int类型),那么该类占的空间大小为1个字节。

如果类中仅包含一个非静态成员和一个静态成员变量,类占的空间大小还是4,因为静态成员变量不在类的对象上。如果再加上一个非静态成员函数或是静态成员函数,类占空间大小还是4因为成员变量和成员函数是分开存储的。如下:

class Person {
public:

	//非静态成员变量占对象空间
	int mA;
	//静态成员变量不占对象空间
	static int mB; 
	//函数也不占对象空间,所有函数共享一个函数实例
	void func() {
		cout << "mA:" << this->mA << endl;
	}
	//静态成员函数也不占对象空间
	static void sfunc() {
	}
};

下面来看this指针。

通过以上的示例可以发现,成员函数不属于对象,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。这时候,就需要用this指针区分是哪个对象调用的自己。例如,现在有两个对象p1,p2。p1调用成员函数PersonAddAge(),那么此时this指针就会指向p1。即this指针指向的是被调用的成员函数所属的对象。

this指针是隐含每一个非静态成员函数内的一种指针,不需要定义,直接使用即可。上述操作都是编译器隐式实现的,我们不需要使用。

如果需要使用this指针,它的作用有以下两个:

1、当形参和成员变量同名时,可用this指针来区分。这个在下面的代码中的参数构造函数中得以体现。

2、在类的非静态成员函数中返回对象本身,可使用return *this,这个在下面代码中的成员函数中得以体现。

#include<iostream>
using namespace std;

class Person
{
public:
    int age;
    
    Person(int age)
    {
        this->age = age;            //这里,形参age和成员变量age同名,如果写成age = age就会
    }                               //出错,因此需要用this指针,当然也可以修改形参名称

    Person& PersonAddAge(Person p)  //这里返回的值是对象本身,所以是Person&
    {                               //!!!注意,如果这里使用的是Person,返回的值就不是p2
        this->age += age;           //会是编译器为你重新创建的对象
        return *this; 
    }

};


void test1()
{
    Person p1(10);

    Person p2(10);

    p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);   //这是一种链式编程思想
                                             //其实和cout的思想差不多,每次调用完返回
    cout<<"p2的年龄为"<<p2.age<<endl;         //p2本身,这样就可以继续往下调用
}

int main()
{
    test1();

    system("pause");
    return 0;
}

 (10)空指针访问成员函数

c++中空指针也可以访问成员函数。只不过要注意此时有没有用到this指针。见下例

#include<iostream>
using namespace std;

class Person {
public:

	int m_Age;

	void ShowClassName() 
    {
		cout << "我是Person类!" << endl;
	}

	void ShowPerson() 
    {
		cout << m_Age << endl;
	}


};

void test1()
{
	Person * p = NULL;
	p->ShowClassName();      //空指针,可以调用成员函数
	//p->ShowPerson();       //解注释会报错,因为成员函数中用到了this指针
}                            //相当于此时的ShowPerson函数中,系统默认使用this->m_Age。但是p是
                             //空指针,类似于你没有女朋友,何谈女朋友的年龄(?)
int main() {

	test01();

	system("pause");

	return 0;
}
(11)const修饰成员函数

常函数:成员函数后加const后我们称为这个函数为常函数。常函数内不可以修改成员属性,但成员属性声明时加关键字mutable后,在常函数中依然可以修改。

在这里说一下this指针的本质,this指针的本质是指针常量,当你调用一个对象的成员函数时,指针的指向不能修改。它相当于Person *const this。const修饰成员函数后,成员函数内就不能再修改成员变量了,也就是this指针指向的值也不能修改了,此时相当于const Person *const this。语法为在成员函数后面加const。

常对象:声明对象前加const称该对象为常对象,常对象只能调用常函数,常对象不可以修改成员属性,但成员属性声明时加关键字mutable后,常对象就可以修改了。

#include<iostream>
using namespace std;

class Person
{
public:
    int m_A;
    mutable int m_B;
 
    void showPerson() const  //这是一个常函数
    {
        //this->m_A = 100;   //这句代码解注释会报错,因为常函数不能修改指针指向的值
        this->m_B = 100;     //如果仍然想修改,就在成员属性前加mutable
    }

    void func()
    {

    }
};

void test1()
{
    Person p;
    p.showPerson();
}

void test2()
{
    const Person p;       //这是一个常对象
    //p.m_A = 100;        //这句代码解注释会报错,因为常对象不能修改普通成员变量的值
    p.m_B = 100;          //如果仍然想修改,就在成员属性前加mutable

    //常对象只能调用常函数
    //p.func();           //这句代码解注释会报错,因为常对象不能调用普通函数      
}                         //因为普通函数可以修改属性,而常对象是不允许的,二者相悖

int main()
{
    test1();

    test2();

    system("pause");
    return 0;
}

3、友元

生活中你的家有客厅(Public),有你的卧室(Private)。

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。

我们已经知道,类的私有属性是不能在类外访问的,但有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。友元的目的就是让一个函数或者类访问另一个类中私有成员。

友元的关键字是friend。有三种实现方式:

1、全局函数做友元

2、类做友元

3、成员函数做友元

(1)全局函数做友元

全局函数做友元,就是让全局函数能够访问类的私有属性。实现方式只需要把全局函数的声明放到类的开头。并且在声明前加上friend。正如示例中的friend void goodGay(Building *building);一样。

#include<iostream>
#include<string>
using namespace std;

class Building
{
    friend void goodGay(Building *building);  //告诉编译器 goodGay全局函数 是 Building类的 
                                              //好朋友,可以访问类中的私有内容
public:
    string m_SittingRoom;

private:
    string m_BedRoom;

public:
    Building()
    {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
};

void goodGay(Building *building)
{
    cout<<"好基友全局函数 正在访问 :"<< building->m_SittingRoom<<endl;

    cout<<"好基友全局函数 正在访问 :"<< building->m_BedRoom<<endl;    //是友元,所以可以访问
}

int main()
{
    Building b;
    goodGay(&b);

    system("pause");
    return 0;
}
(2)类做友元

类做友元就是让类能访问另一个类的私有属性。实现方式只需要把类的声明放到类的开头。并且在声明前加上friend。正如示例中的friend class GoodGay;一样。

#include<iostream>
#include<string>
using namespace std;

class Building
{
    friend class GoodGay;

public:
    string m_SittingRoom;      //客厅

private:
    string m_BedRoom;          //卧室

public:
    Building();                //这里是类外实现成员函数,所以类内只用写函数声明

};

class GoodGay
{
public:
    GoodGay();

    void visit();

    Building *building;
}

//类外实现成员函数
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodGay::GoodGay()
{
    building = new Building;
}

void GoodGay::visit()
{
    cout<<"好基友函数 正在访问 :"<< building->m_SittingRoom<<endl;

    cout<<"好基友函数 正在访问 :"<< building->m_BedRoom<<endl;    //是友元,所以可以访问
}

int main()
{
    GoodGay gg;
    gg.visit();

    system("pause");
    return 0;
}
(3)成员函数做友元

成员函数做友元就是让成员函数能访问另一个类的私有属性。实现方式只需要把成员函数的声明放到类的开头,并且要标注该函数所属的作用域/所属类,在这个案例里就是加上GoodGay::。并且在声明前加上friend。正如示例中的friend void GoodGay::visit();一样。

#include<iostream>
#include<string>
using namespace std;

class Building
{
    friend void GoodGay::visit();

public:
    string m_SittingRoom;      //客厅

private:
    string m_BedRoom;          //卧室

public:
    Building();                //这里是类外实现成员函数,所以类内只用写函数声明

};

class GoodGay
{
public:
    GoodGay();

    void visit();               //让该函数可以访问Building中的私有成员
    void visit2();              //让该函数不可以访问Building中的私有成员

    Building *building;
}

//类外实现成员函数
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodGay::GoodGay()
{
    building = new Building;
}

void GoodGay::visit()
{
    cout<<"visit函数 正在访问 :"<< building->m_SittingRoom<<endl;

    cout<<"visit函数 正在访问 :"<< building->m_BedRoom<<endl;    //是友元,所以可以访问
}

void GoodGay::visit2()
{
    cout<<"visit2函数 正在访问 :"<< building->m_SittingRoom<<endl;

    //cout<<"visite2函数 正在访问 :"<< building->m_BedRoom<<endl;   //不是友元,不能访问 
}

int main()
{
    GoodGay gg;
    gg.visit();
    gg.visit2();

    system("pause");
    return 0;
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐