【C++】类和对象之类内函数

2022/7/25 1:54:08

本文主要是介绍【C++】类和对象之类内函数,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

0 前言

本小节是构造函数与成员变量相关的笔记。
包含:

  1. 函数:默认构造函数、拷贝构造函数、类型转换构造函数、移动构造函数(待写)、析构函数、静态成员函数
  2. 重载:运算符重载(简略)、函数重载
  3. 函数其他:对象的构造与析构顺序、重写重载和覆盖、浅拷贝
  4. 变量:列表初始化、初始化顺序、成员变量的内存对齐

平时都在用,但是总是判断错误:在成员函数中可以访问同类对象的所有属性!

1 默认构造函数

1.1 默认构造函数的重要性

默认构造函数指的是无参构造函数,在有些情况下非常重要:如创建的对象数组不能全部初始化,后续的需要默认初始化;再如被组合到其他类中的类,没有默认初始化则对应的类也必须为其初始化。
因此定义类的同时尽量要定义一个默认构造函数。

1.2 默认构造函数的创建

class ObjectFunc
{
   ObjectFunc()
   {
      ...
   }
}

只要提供了一个构造函数(无参、有参、拷贝等),编译器就不会默认合成一个构造函数。若需要编译器自动合成的默认构造函数,可通过ObjectFunc()=default;声明实现。
构造函数一般设置为公有public,否则在main函数中无法调用进行初始化。

#include <iostream>
#include <vector>
#include <ostream>
using namespace std;

class ObjectFunc
{
public:
//	ObjectFunc()=default;
	ObjectFunc(const ObjectFunc& a)
	{
		this->v = a.v;
	}
	private:
	int v;
	int t = 9;
	char d{'a'};
};
int main()
{
	ObjectFunc a; //报错:类ObjectFunc不存在默认构造函数
	return 0;
}

2 拷贝构造函数

2.1 拷贝构造函数的重要性

在很多场景下需要使用一个对象来初始化另一个对象,若不定义拷贝构造函数,则编译器自动合成一个拷贝构造函数。编译器自动合成的拷贝构造函数执行浅拷贝,成员变量含有指针时,则会使两个指针指向同一片区域,一个对象的析构会释放此空间。因此,若成员变量为指针时,一定要定义拷贝构造函数。(也有利用浅拷贝的,比如智能指针中的共享指针share_ptr)

2.2 拷贝构造函数的用法与用途

  • 特点:只有一个参数,必须为引用类型,常常使用const对参数修饰

  • 用法:

    ObjectFunc(const ObjectFunc& a)
    {
     ...//执行拷贝赋值
    }
    
  • 用途(3个):
    使用同类对象初始化时:=、()、{}方式初始化都会调用
    传递类对象参数为值传递方式时:void exampleFunc(ObjectFunc a)
    返回值为类对象时:ObjectFunc exampleFunc1(){return ObjectFunc();}

      //class ObjectFunc
      ObjectFunc(const ObjectFunc& a)
      {
      	this->v = a.v;
      	cout << "调用拷贝构造函数" << endl;
      }
      
      //main
      //初始化
      ObjectFunc b = a;
      ObjectFunc c(a);
      ObjectFunc d{ a };
      
      
      //返回值
      ObjectFunc exampleFunc1()
      {
      	ObjectFunc d;
      	return d;
      }
      //值传递
      void exampleFunc2(ObjectFunc a)
      {
      	;
      }
    

2.3 拷贝构造函数的注意事项

  • 为什么传递参数不能为传值?
    因为值传递同样需要构造一个新的类对象,这造成无限循环调用拷贝构造函数。

  • 初始化与赋值的区分
    类对象的初始化和赋值都使用等号,区别在于初始化是定义的同时给予初值,调用了拷贝构造函数;而赋值直接执行浅拷贝。

    ObjectFunc a;
    ObjectFunc b = a;//定义并初始化
    ObjectFunc c;
    c = a;//赋值
    
  • 如何更好的避免浅拷贝?
    类内定义了指针时,要考虑定义拷贝构造函数、赋值重载函数、以及析构函数。这三个函数是绑定在一起的。

  • 临时变量

    ObjectFunc();
    ObjectFunc obj = ObjectFunc();
    

    单独说一下,上述代码并不是仅仅是函数调用,同时还构造了一个临时变量。若此临时变量有其他定义的变量直接承接,相当于临时变量直接转正,不存在拷贝构造函数的调用。

2.4 右值减少拷贝

在类和对象之基础中已经对右值引用以及使用做了简单的笔记。右值引用重点针对的是即将销毁的对象或者右值,如临时对象、不再使用的变量、表达式。在函数中常常将局部变量创建临时对象方返回,调用了拷贝构造函数。在此处可以使用右值引用+std::move(局部变量)就不需要调用拷贝构造函数了。

3 类型转换构造函数

3.1 用法

  • 使用时机:当类中存在只有一个参数的构造函数时,就可以在初始化=、值传递、返回值等场合,只使用参数。编译器会自动调用类型转换构造函数,对对象进行初始化或者创建临时变量。
  • 具体用法:
    //class ObjectFunc
    ObjectFunc(string s)
    {
    	++i;
    	cout << "ObjectFunc调用次数:" << i << endl;
    }
    static int i;
    int ObjectFunc::i = 0;//类外
    
    //func
    void typeConverFunc(ObjectFunc s)
    {
    	;
    }
    ObjectFunc returnTypeConverFunc(string& s)
    {
    	return s;
    }
    
    //main
    ObjectFunc g = string("asd");//初始化
    
    typeConverFunc(string("dfgh"));//值传递
    
    string s{ "zxcv" };//返回值
    returnTypeConverFunc(s);
    
    //output
    ObjectFunc调用次数:1
    ObjectFunc调用次数:2
    ObjectFunc调用次数:3
    

3.2 explicit关键字

当构造函数有一个参数时,可以被作为类型转换构造函数,通过类型转换创建一个实例。若不想让其进行类型转换,就可以在只有一个参数的构造函数前添加explicit关键字,从而禁止调用类型转换构造函数。

explicit ObjectFunc(string s)
{
	++i;
	cout << "ObjectFunc调用次数:" << i << endl;
}
ObjectFunc returnTypeConverFunc(string& s)
{
	return s;//不存在用户定义的从string到ObjectFunc的转换
}

在使用explicit关键字时,.h文件的成员函数声明前可使用,但是类外的成员函数定义处就不能再使用了。
string类中没有定义explicit关键字,因此可以从字符串到string类的转换,但是vector容器中定义了ecplicit,因此就不能类型转换。

3.3 注意事项

对象数组使用参数初始化时,参数部分调用了类型转换构造函数,而后续没有初始化的部分调用的是默认构造函数。
ObjectFunc obj[10] = {"qwe", "asd", "fgh"};

4 析构函数

4.1 析构函数的重要性

若不自行定义析构函数,编译器会自动创建一个析构函数。若类中定义的成员变量都是内置类型,那么使用默认析构函数就能在对象被销毁时自动回收内置类型成员变量的空间。但是如果存在指针,那么默认析构函数只能回收指针变量的空间,而指针所指向的空间没有被释放,存在内存泄漏。因此,拷贝构造函数、赋值运算符重载以及析构函数一般是同时定义。

4.2 析构函数的用法

析构函数要定义为public,若不自行定义析构函数,编译器会自动合成一个析构函数,可能造成内存泄漏。

~ObjectFunc()
{
	delete pobj;
}

4.3 继承中的构造与析构

在继承中,子类不仅要初始化自行定制的成员变量,还要初始化父类定义的成员变量(不论是否能够访问到),因此在析构时父类与子类的析构函数都要调用。为了达到这个目的,在继承使用中,析构函数一定要定义成虚函数,而后先调用子类的析构函数,再调用父类的析构函数。

图片名称

5 移动构造函数

待写...

6 运算符重载函数

6.1 运算符重载的意义

C++中的类型决定了分配多少内存空间、如何解释此部分内存空间的数值、此数值能够进行运算。内置类型如整型变量能够进行加减乘除、赋值、比较大小、输入以及输出等。为了让自定义的类型也能使用运算符进行操作,因此对运算符进行重载。
这些运算符重载本质上是一次函数调用,因此也归纳到本小节的笔记中。

6.2 运算符重载的规则

  • 重载运算符的返回类型通常情况下应该与内置版本的返回类型兼容:

    运算符 返回类型
    关系或者逻辑 bool
    算术 类类型值
    赋值、复合赋值 左侧对象引用
  • 在定义的时候往往定义一套,如定义了>理应定义其他的运算符;定义了==理应在此基础上定义!=;定义了+=理应在此基础上定义=

6.3 运算符重载的分类

运算符 要求
. :: .* ?: 不能被重载
= [] () -> 必须定义为成员函数
>> << 必须定义为全局
对称性运算符如算术、逻辑、关系 尽量定义成全局函数
紧贴对象的运算符如* ++ -- 尽量定义成成员

综上所述,四个不能重载、四个必须定义为成员、两个必须定义为全局,其余的都是建议。

  • 为什么对称性的尽量定义为全局呢?比如string的+运算符定义为成员,则只能进行string+"casd"形式,反过来却不行。但是定义为全局则string+"asd"或者"asd"+string都可。

  • 逻辑运算符&&||重载后,没有此运算符的短路规则,因为本质上是函数调用。

6.4 两种重载方式

重载为全局函数(声明为友元)和成员函数。

class ObjectFunc
{
	friend ostream& operator<<(ostream& os, const ObjectFunc& object);
public:
//重载为成员函数
	ObjectFunc& operator+=(const ObjectFunc& a)
	{
		this->v += a.v;
		return *this;
	}
private:
	int v;
};
//重载为全局函数,要声明为友元
ostream& operator<<(ostream& os, const ObjectFunc& object)
{
	os << object.v;
	return os;
}

6.5 参考资料位置

《C++ Primer 第5版》

重载运算符 位置
输入输出 P494
算术关系 P497
赋值 P499
下标 P501
递增递减 P502
成员访问 P504
函数调用 P506

7 函数重载

7.1 函数重载的意义

函数重载在各种编程语言中都很常见,使用同一个函数名传递不同的参数(数量或者类型不同)实现不同的功能。

7.2 函数重载的用法

特点:函数名相同,参数(数量或类型)不同。返回值可同可不同。

在Cpp中,函数在编译过程中函数名转化为函数名+参数数量+参数类型命名的全局函数,同样成员函数也是如此,只是多了类名而已,类名+函数名+参数数量+参数类型。因此确定一个函数的主要标志就是函数名与参数
因此,函数名相同,但是参数数量或者类型不同的函数就可以重载,而返回值可以不相同。

int sameFunc(int i, int j)
{
	return i + j;
}

char sameFunc(int i)
{
	return i;
}

cout << sameFunc(1, 2) << endl; //3
cout << sameFunc(99) << endl; // 'c'

7.3 成员函数的各种重载形式

主要是const形式的重载。

  1. 函数参数为const引用算重载:void func(const ObjectFunc& a)void func(ObjectFunc& a)
  2. const成员函数与非const算重载:void func()constvoid func()
  3. 参数为左值引用和右值引用算重载:void func(ObjectFunc& a)void func(ObjectFunc &&b)

7.4 重载、重写和覆盖的区别

  • 重载:函数名相同,参数类型不同的全局函数或者成员函数
  • 重写:父类为抽象类,子类实现了父类的virtual虚函数成为重写(void func()=0形式为纯虚函数)
  • 覆盖:子类中写了与父类同名的非虚成员函数,子类在调用时会优先调用子类中的函数,子类函数将父类函数覆盖掉。如果需要调用父类的同名非虚函数需要通过父类作用域来调用Father::func()

8 静态函数

在[类和对象之基础]中已经总结过静态函数的笔记。此处主要列出主要的关键字。

  • sizeof: 计算对象占用内存大小时不被计入,因为其不属于任何对象
  • 访问与被访问:由于没有this指针,静态函数只能访问静态成员变量,而不能访问普通成员变量。普通对象可以调用以及访问静态成员函数和静态变量,访问方式有两种,分别是作用域符::和点.
  • this指针:由于没有this指针,因此不能使用const、virtual修饰
  • 初始化:静态成员变量的初始化需要在类外进行,并且不能再添加static关键字

9 成员变量

9.1 初始化与赋值

构造函数中常见的两种给成员变量赋初值的方式,前一种称为列表初始化,后一种是赋值。实际上,后一种先进行了默认初始化,而后在函数体中执行了拷贝赋值,若遇上const、引用、无默认构造的成员,这种方式就会报错。

ObjectFunc():a('a'), pobj(nullptr)
{
	...
}
ObjectFunc(int v)
{
	this->v = v;
}

在具体使用中,初始化与赋值虽然都使用等号=,但是初始化是定义的同时初始化,而赋值是给已定义的对象赋值。

9.2 成员变量的初始化

总结:成员变量要初始化。尤其遇到const、引用、组合形式的无默认构造的其他对象时,要列表初始化,有指针则要定义拷贝构造、赋值运算符重载、析构函数。

  • 非静态内置类型变量(如int i;):若不进行显示初始化,则编译器不进行定义,不使用不出错,一使用就报未定义的错误。因此非静态内置类型变量要初始化,不论是定义时初始化,还是在构造函数函数体前使用列表初始化、还是构造中使用赋值皆可。
  • 静态内置类型变量(如static int v;):若不进行初始化,则编译器自动初始化为0。不过最好还是在类外进行初始化。
  • const成员变量初始化:const成员变量必须在函数体前使用列表进行初始化,否则会报错。同时也不能在构造函数中赋值,会报不能修改的左值。
  • 引用成员变量初始化:同const成员变量,也需要列表初始化。
  • 组合形式使用的其他对象:其他对象有默认构造函数可以,没有的话也必须列表初始化。
  • 指针成员:要定义拷贝构造、赋值运算符重载、析构函数。

9.3 成员变量初始化注意事项

  • 初始化顺序:成员变量的初始化与列表初始化顺序无关,仍然按照变量的定义顺序进行初始化。
  • 内存对齐:和struct相同,成员变量定义时也要考虑内存对齐,将大类型放前面,小类型放后边时可取的。
  • 浅拷贝:常见的发生浅拷贝的场景是类中存在指针成员,但是没有定义拷贝构造函数,或者没有赋值运算符重载的直接赋值操作,执行的都是浅拷贝,最终可能导致内存泄漏。
  • 临时变量:
    ObjectFunc();
    ObjectFunc obj = ObjectFunc();
    
    单独说一下,上述代码并不是仅仅是函数调用,同时还构造了一个临时变量。若此临时变量有其他定义的变量直接承接,相当于临时变量直接转正,不存在拷贝构造函数的调用。

10 顺序

10.1 变量的初始化顺序与析构顺序

  1. 若有全局变量或者静态全局变量,按照定义的顺序先后初始化,而后在所有程序执行完毕,按照先定义的后销毁原则进行析构;
  2. main函数中局部变量初始化,在main函数体}结束时进行析构;
  3. 函数内的static变量在第一次使用时初始化,一直到程序结束再销毁。先定义的后销毁。

10.2 成员函数与成员变量的编译顺序

编译时先编译类的成员变量,而后才编译成员函数,因此即使成员变量定义在成员函数后,成员函数还是能直接引用,而不用声明。
但是使用typedef定义类型时不同,其必须放在类的最初始位置。

10.2 构造函数与析构函数在继承时的调用顺序

先调用父类的构造函数,再调用子类的构造函数;析构时先调用子类的析构函数,而后再调用父类的析构函数。



这篇关于【C++】类和对象之类内函数的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程