自学内容网 自学内容网

C++-第三章:类和对象

目录

第一节:成员的访问权限

        1-1.public

        1-2.private

        1-3.protect

第二节:成员函数声明与定义分离

第三节:封装和this指针

        3-1.封装

        3-2.this指针

        3-3.附加

第四节:类的默认成员函数

        4-1.默认构造函数

                4-1-1.默认初始化函数

        4-1-2.默认拷贝函数

        4-2.默认析构函数

                4-3.默认拷贝赋值运算符函数

下期预告:


第一节:成员的访问权限

        一般情况下,一个类由成员变量和成员函数组成(如果有),为了实现对类中某些重要数据的保护,类中成员变量的访问和修改,成员函数的调用都可以设置public、private、protect三种权限。

        1-1.public

        public为公有权限,它修饰的成员变量允许在任何地方被修改,它修饰的成员函数允许在任何地方被调用:

#include <iostream>
class Test
{
public: // 修饰它下面的成员,直到遇到另一个访问限定符为止
int a = 18;
int Add(int x,int y)
{
return x + y;
}
};
int main()
{
Test t;
std::cout << t.a << " " << t.Add(1, 2) << std::endl;
return 0;
}

 

        

        1-2.private

        private私有权限,它修饰的成员变量只允许在自己的成员函数内部被访问和修改,它修饰的成员函数也只允许被自己的成员函数调用:

        使用private修饰后再在外部访问成员 a 和 Add 就会报错,如果此时要在不改变权限的情况下得到a的值、修改a的值和使用Add函数,可以在类中定义一些public函数来修改、调用它们:

class Test
{
private: // 修饰它下面的成员,直到遇到另一个访问限定符为止
int a = 18;
int Add(int x,int y)
{
return x + y;
}
public:
int Get_a() // 得到a的值
{
return a;
}
void Mod_a(int x) // 修改a的值
{
a = x;
}
int Call_Add(int x,int y) //调用 Add 函数
{
return Add(x,y);
}
};

         因为上述3个public函数都在类的内部,即使a和Add用private修饰了也是能访问到的,此时就可以经由这3个public函数之手操作它的private成员:

int main()
{
Test t;
std::cout << t.Get_a() << " " << t.Call_Add(1, 2) << std::endl;
return 0;
}

  

        1-3.protect

        protect保护权限,它的权限和private一模一样,只在类的继承中有意义,类的继承以后会讲到。

        注意:class和struct都有默认权限,class的默认权限是private,struct的默认权限是public,这是它们唯一的区别。

第二节:成员函数声明与定义分离

        成员函数也支持声明与定义分离,声明放在类中,定义需要指明类域:

class Test
{
public:
int a = 18;
int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{
return x + y;
}

        定义中的变量会有匹配优先级:局部域>类域>全局域,类域就是实例自己的空间,Test类中有一个变量a,假如在函数中也定义一个同名变量a,根据优先级它就会优先匹配新定义的a:

#include <iostream>
class Test
{
public:
int a = 18;
int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{
int a = 1; // 局部域的a
std::cout << "a=" << a << std::endl;
return x + y;
}
int main()
{
Test t;
t.a = 0; // 初始化类域中的a
t.Add(1, 2);
return 0;
}

 

       如果局部域没有同名变量,它就会使用类域中的变量:

int Test::Add(int x, int y) // 定义,需要指明类域
{
//int a = 1; // 局部域的a
std::cout << "a=" << a << std::endl;
return x + y;
}

 

        如果要使用全局域的变量,可以在变量前加::,表示属于全局域的变量:

int a = 2; // 全局域的a
class Test
{
public:
int a = 18;
int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{
//int a = 1; // 局部域的a
std::cout << "a=" << ::a << std::endl; // a前加::,在全局域中找变量a
return x + y;
}

 

        那么如果局部域中有类域中的同名变量,那么要怎么访问类域中的变量呢,接下来讲了类的封装和this指针问题就迎刃而解了。

        

第三节:封装和this指针

        3-1.封装

        封装是面向对象的三大特性之一,它实际上是对数据的一种管控:

        (1)将数据和函数都放在类中

        (2)用访问限定符对成员变量和函数进行限定

        例如第一节对private成员进行使用和修改的那3个public函数就是一种封装,因为我们只能通过这个类提供的函数来合法的操作里面的数据,就是对数据的一种管控和保护。

        另外,封装还能屏蔽底层细节,就是一个函数可以调用一个或者多个函数。

        例如如果我们需要得到 x*y+z 的值,我们就可以把它分成乘、加两个子任务,就可以写好完成乘的Mul函数和完成加的Add函数,然后设置成private函数由 Mul_Add 函数统一调用即可:

class Test
{
private:
int Add(int x, int y)
{
return x + y;
}
int Mul(int x, int y)
{
return x * y;
}
public:
int a = 18;
int Mul_Add(int x,int y,int z)
{
return Add(Mul(x, y), z);
}
};
int main()
{
Test t;
std::cout << t.Mul_Add(1,2,3);
return 0;
}

 

        上述代码我们在外部只调用了Mul_Add函数,让它自己调用Mul和Add函数,这种也叫封装,它的作用是屏蔽底层代码细节,使用某一个功能时更方便;

        且对于复杂的需求,就可以把它分成几个子任务,单独完成后再统一调用,这样的代码不仅可读性强、解耦性好、还易于维护。

        3-2.this指针

        之前我们说过类的函数是统一存放在公共区域的,每个实例调用的都是同一个函数,那么请看如下代码预测结果:

#include <iostream>
class Test
{
public:
int a;
void Print()
{
std::cout << a << std::endl;
}
};
int main()
{
Test t1;
Test t2;

t1.a = 0;
t2.a = 1;

t1.Print();
t2.Print();
return 0;
}

 

        我们发现实例调用函数时,打印的是自己的变量a的值,但是调用的不都是公共区域的函数吗,为什么结果却不一样呢?

        这是因为类的函数有一个隐含的参数——this指针,我们可以显示的把它写出来,即:

void Print(Test* this) // 显示的写出this指针
{
std::cout << a << std::endl;
}

        this的类型就是函数所在类的类型的指针,在具体的实例调用函数时,除了本来的参数,还会自动的把自己的地址也传给这个参数。

        上述语句块的a,实际上也被省略了前缀,完整的是:this->a,访问的是具体实例中的变量,所以不同实例调用同一个函数却能打印出自己的变量的值。

t1.Add() 等价于 t1.Add(&t1)

        类的实例与成员函数的关系如下图:

        3-3.附加

        一个类的成员函数因为存放在一个公共区域,所以函数不计算实例和类的大小,而如果一个类或者实例没有成员变量,它的大小也不会是0,在vs2022中是1,因为没有体积的类是无意义的,占用1字节来表示该类是存在的。

        其次,如果一个类有成员变量,它的大小计算规则与C语言的结构体相同。

        对于类来说,成员变量前一般加_ 符号,让它与函数的参数分开,这是一种优良的代码习惯。

        

第四节:类的默认成员函数

        默认成员函数就是定义一个类时会默认生成的函数,你不实现,它就存在于类中。

        但你一旦实现,就不会生成。

        4-1.默认构造函数

        默认构造函数具有以下3个特征:

        1、函数名与类型相同。

        2、无返回值,无返回值指函数连返回类型都不需要写出来,而不是返回void。

        3、类在实例化时自动调用对应的构造函数。

        默认构造函数有默认初始化函数和默认拷贝构造函数,如果不实现任何构造函数,编译器会默认生成一个默认初始化函数+一个默认拷贝函数,只要实现了其中任何一个,两个默认函数都不会生成。

        默认构造函数或者构造函数都不是开辟空间,而是初始化实例中的各种成员变量。

                4-1-1.默认初始化函数

       默认初始化函数的"样子"如下:

class Test
{
public:
    Test()
    {
        // 语句;
    }
};

        实际上默认成员函数是不能看见的,写出来只是帮助理解,如果像上述代码中这样写出来的话默认构造函数就不会生成了。

        初始化函数的调用时机是固定的,都是在创建一个新对象时调用:

class Test
{
public:
int a;
void Print()
{
std::cout << a << std::endl;
}
};
int main()
{
Test t1; // 调用默认构造
Test t2; // 调用默认构造
return 0;
}

        看起来好像只是创建了实例,而没有调用任何函数,这是因为默认的初始化函数只作一个功能,且默认初始化函数就是不需要传任何参数:1、对于内置类型成员不作任何处理;2、对于自定义类型成员调用它的默认初始化函数。

        内置类型就是int、char等语言自带的类型,自定义类型就是类和结构体类型。

        实际上,对于一个类一般都会自己实现一个初始化函数,例如一个student类,我们想在创建实例时就确定它的学号、年龄:

#include <string>
class student
{
public:
student(const char* name,int age)
{
_name = name;
_age = age;
}
std::string _name;
int _age;
};
int main()
{
student Eric("Eric", 18); // 调用初始化函数初始化内置类型
return 0;
}

        初始化函数还允许重载,以适应不同的初始化需求:

#include <string>
class student
{
public:
student(const char* name,int age)
{
_name = name;
_age = age;
}
student(const char* name) // 仅初始化名字
{
_name = name;
}
student(int age) // 仅初始化年龄
{
_age = age;
}
std::string _name;
int _age;
};
int main()
{
student Eric("Eric", 18); // 调用初始化函数初始化内置类型
student Bob("Bob"); // 调用初始化函数初始化内置类型
student Jack(18); // 调用初始化函数初始化内置类型
return 0;
}

        4-1-2.默认拷贝函数

        默认拷贝函数也属于默认构造函数,它的功能是可以用一个实例来初始化另一个实例,:

#include <iostream>
#include <string>
class student
{
public:
std::string _name;
int _age;
};
int main()
{
student Bob;
Bob._name = "Bob";
Bob._age = 20;
student Bob_copy(Bob); // 调用拷贝函数初始化内置类型
std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;
return 0;
}

 

        默认拷贝构造会对内置类型作值拷贝,对于自定义类型会调用它的拷贝构造函数。

        因为上述代码中Bob_copy是用Bob初始化的,所以它们的内容是一样的。

        拷贝构造函数也可以重载,一般用引用+const接收实例:

        但是当我实现了拷贝构造之后,就不能创建实例了,这是因为拷贝构造函数也属于构造函数,实现了图中构造函数之后默认初始化函数也不会自动生成了,所以需要自己再写一个初始化函数:

#include <iostream>
#include <string>
class student
{
public:
student() // 无参的初始化函数,不作任何处理
{}
student(const student& d) // 拷贝构造
{
_name = d._name;
_age = d._age;
}
std::string _name;
int _age;
};
int main()
{
student Bob;
Bob._name = "Bob";
Bob._age = 20;
student Bob_copy = Bob; // 拷贝函数初始化内置类型
std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;
return 0;
}

 

        当然自己写的拷贝构造函数也不用拷贝所有的内置类型,可以只拷贝年龄等,可以按具体的需求写拷贝构造函数的语句项,非常灵活。

        拷贝构造除了用()调用,还可以用 = 来调用:

int main()
{
student Bob;
Bob._name = "Bob";
Bob._age = 20;
student Bob_copy = Bob; // 用=调用拷贝函数初始化内置类型
std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;
return 0;
}

 

        所以在对类进行传值调用时,形参也是实参的一份拷贝,即:形参 = 实参,所以会调用一次拷贝构造:

void func(student x)
{
return;
}
int main()
{
student Bob;
func(Bob);
return 0;
}

  

        这次拷贝构造就是在传参:student x = Bob 时调用的。

        总结:当不实现任何构造函数时(初始化函数、拷贝函数),编译器会默认生成一个默认初始化函数+一个默认拷贝函数,只要实现了其中任何一个,编译器两个默认函数都不会生成。

        默认初始化函数是不需要传参(无参数或者全缺省)的函数,对内置类型不作处理,对自定义类型调用它的初始化函数;

        默认拷贝函数是带两个参数(隐含的this指针+一个实例)的函数,对内置类型作值拷贝,对自定义类型调用它的拷贝函数。

        4-2.默认析构函数

        析构函数的作用是在实例的生命周期终结的时候自动调用,它的功能不是销毁类而是清理类中的资源,它具有以下特征:

        1、函数名是类名前加"~"

        2、无参数;无返回值

        3、一个类只能有一个析构函数,未显示声明系统会生成一个,所以析构函数不能重载

        4、实例的生命周期结束时会自动调用

        5、默认析构函数对内置类型不作处理,对自定类型会调用它的析构函数

        析构函数也可以显示的写出来,一个析构函数的"样子"如下:

class Test
{
public:
    ~Test()
    {
        // 语句;
    }
};

        如果实例都在栈上,而栈具有"先进后出"的特性,所以在程序结束时,先定义的实例反而后销毁:

#include <iostream>
#include <string>
class student
{
public:
student(const char* name,int age) // 初始化函数
{
_name = name;
_age = age;
}
student(const student& d) // 拷贝构造,但是不使用引用
{
std::cout << "调用了一次拷贝构造" << std::endl; // 打印提示信息
_name = d._name;
_age = d._age;
}
~student() // 析构函数
{
std::cout << _name << "被销毁了" << std::endl;
}
std::string _name;
int _age;
};
void func(student x)
{
return;
}
int main()
{
student Bob("Bob", 20);
student Eric("Eric",18);
return 0;
}

  

        不同区域的类的销毁顺序如下:

        局部变量->局部静态变量->全局静态变量和全局变量。

        4-3.默认拷贝赋值运算符函数

        拷贝赋值运算符函数与拷贝函数不同,一个在实例定义之后使用,一个在实例定义时使用,讲拷贝赋值运算符之前,我们先看看它是怎么使用的:

int main()
{
student Bob("Bob", 20);
student Eric("Eric",18);
Eric = Bob; // 拷贝赋值运算符
return 0;
}

  

        虽然这好像是用了赋值符号=,而没有调用函数,实际上对于自定义类型这就是一种函数调用的方式,这和operator关键字有关,下一章我们就会讲。

        就和内置类型的赋值一样,Bob将自己的数据拷贝了一份给Eric,所以程序结束时有两个"Bob"。

        默认拷贝赋值函数会做的工作如下:

        对内置类型进行只拷贝,对自定义类型调用它的拷贝赋值函数。

        它具有以下特点:

        1、一般一个类只有一个拷贝赋值函数

        2、如果不实现拷贝赋值函数,编译器就是生成一个默认拷贝赋值函数

        关于如何实现一个拷贝赋值函数,这和operator关键字有关,下一章讲详细叙述。

                

下期预告:

        下一期还是默认成员函数的内容:

        1、operator关键字

        2、取地址函数重载

        3、初始化列表

        4、类的隐式类型转换        


原文地址:https://blog.csdn.net/2303_78095330/article/details/142517336

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!