C++初阶(五)--类和对象(中)--默认成员函数

目录

一、默认成员函数(Default Member Functions)

 二、构造函数( Constructor)

1.构造函数的基本概念

2.构造函数的特征

3.构造函数的使用

无参构造函数 和 带参构造函数

注意事项:

4.默认构造函数

隐式生成的默认构造函数:

默认构造函数的特性 :

注意事项:

5.构造函数的特性

三、析构函数(Destructor)

1.析构函数的基本概念

2.析构函数的特征

3.析构函数的使用

4.默认析构函数

5.析构顺序问题

6.析构函数特性

四、 拷贝构造函数

1.定义

2.拷贝构造函数的特性

3.拷贝构造函数的用法

对象初始化

函数返回值按值返回

4.拷贝构造函数的深拷贝与浅拷贝

5.引用传参(面试题)


一、默认成员函数(Default Member Functions)

如果一个类中什么成员都没有,简称为空类
空类中真的什么都没有吗?并不是,任何类在什么都不写时编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

class Date {};

 二、构造函数( Constructor)

1.构造函数的基本概念

构造函数是一种特殊的成员函数,它在对象创建时自动调用,用于初始化对象的成员变量。构造函数的名字必须与类名完全相同,并且没有返回类型。

注意:构造函数只是初始化对象,不开辟空间创建对象。

2.构造函数的特征

构造函数是特殊的成员函数,主要特征如下:

  • 构造函数的函数名和类名是相同的 (比如类名是 Date,构造函数名就是 Date)
  • 构造函数无返回值 (它不具有返回类型,因此不能直接返回值)
  • 构造函数支持重载 (仔细看下面的例子)
  • 会在对象实例化时自动调用对象定义出来。

3.构造函数的使用

 无参构造函数 和 带参构造函数

#include <iostream>  
using namespace std;

class MyClass 
{
public:

    MyClass() 
    {
         cout << "无参构造函数被调用" << endl;
    }

    MyClass(int val)
    {
        value = val;
        cout << "带参数的构造函数被调用,值为: " << value << endl;
    }
private:
    int value;

};

int main() 
{
    MyClass obj1; // 调用无参构造函数,注意后面没有括号哦~  
    MyClass obj2(10); // 调用带参数的构造函数  
    return 0;
}

运行结果:

解读:不给参数时就会调用 无参构造函数,给参数则会调用 带参构造函数。 

注意事项:

 构造函数是特殊的,不是常规的成员函数,不能直接调

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 0) 
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main(void)
{
    Date d1;
    d1.Date(); // 不能这么去调,构造函数是特殊的,不是常规的成员函数!

    return 0;
}

 

 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。 

#include <iostream>  
using namespace std;

class MyClass 
{
public:

    MyClass() 
    {
         cout << "无参构造函数被调用" << endl;
    }

    MyClass(int val)
    {
        value = val;
        cout << "带参数的构造函数被调用,值为: " << value << endl;
    }

    void Print()
            {
                cout << "lllll" << endl;
            }

private:
    int value;

};

int main() 
{
    MyClass obj1(); // 调用无参构造函数  
    obj1.Print();
    MyClass obj2(10); // 调用带参数的构造函数  
    obj2.Print();
    return 0;
}

 带上括号:

不带括号:

 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。 

 如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。 

class Date 
{
public:
    //如果用户显式定义了构造函数,编译器将不再生成
   /* Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }*/
    void Print()
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

private:
    int _year;
    int _month;
    int _day;
};

int main(void)
{
    Date d1;
    d1.Print();
    return 0;
}

 输出结果:

没有定义构造函数,对象也可以创建成功,因此此处调用的是 编译器默认生成的构造函数。

接下来,我们来讲一下默认构造函数。 

4. 默认构造函数

概念:

默认构造函数是一种没有参数或参数全部有默认值的构造函数。当没有为类显式定义任何构造函数时,编译器会自动生成一个默认的无参构造函数。这个构造函数通常不会执行任何初始化操作,但它是存在的,可以用于创建类的对象。

无参构造函数、全缺省构造函数、自动生成的构造函数都被称为 默认构造函数

并且 默认构造函数只能有一个!

隐式生成的默认构造函数:

#include <iostream>  
using namespace std;

class MyClass 
{
public:
    // 没有显式定义任何构造函数  
};

int main() 
{
    MyClass obj; // 调用隐式生成的默认构造函数  
    return 0;
}

在这个例子中,MyClass没有显式定义任何构造函数,因此编译器会为其生成一个默认的无参构造函数。在main函数中创建MyClass类型的对象obj时,会调用这个隐式生成的默认构造函数。

默认构造函数的特性 :

  1. 无参或全默认参数:默认构造函数要么没有参数,要么所有参数都有默认值。
  2. 自动生成:当类中没有显式定义任何构造函数时,编译器会自动生成一个默认构造函数。
  3. 初始化成员变量:虽然隐式生成的默认构造函数不会执行任何自定义的初始化操作,但它仍然会调用成员变量的默认构造函数(如果成员变量是类的对象)。

成员变量的默认构造函数调用 :代码示例

#include <iostream>  
using namespace std;

class Member 
{
public:
    Member() 
    {
        cout << "Member默认构造函数被调用" << endl;
    }
};

class MyClass 
{
private:
    Member member;
public:
    // 没有显式定义任何构造函数  
};

int main() 
{
    MyClass obj; // 调用隐式生成的默认构造函数,同时调用Member的默认构造函数  
    return 0;
}

 输出:

在这个例子中,MyClass包含了一个Member类型的成员变量。当创建MyClass类型的对象时,隐式生成的默认构造函数会调用Member的默认构造函数来初始化member。 

注意事项:

语法上无参和全缺省可以同时存在,但如果同时存在会引发二义性:

class Date 
{
public:
    Date() 
    {
        _year = 1970;
        _month = 1;
        _day = 1;
    }
    Date(int year = 1970, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main(void)
{
    Date d1;
    return 0;
}

5.构造函数的特性

通过刚才的讲解我们知道了任何一个类的默认构造函数,只有三种:

  • 无参的构造函数
  • 全缺省的构造函数
  • 我们不写,编译器自己生成的构造函数

如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。

代码:

class Date 
{
public:
    // 让编译器自己生成一个
    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

private:
    int _year;
    int _month;
    int _day;
};

int main(void)
{
    Date d1;  // 这里调用的是默认生成的无参的构造函数
    d1.Print();

    return 0;
}

关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d1对象调用了编译器生成的默 认构造函数,但是d1对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用?

解答:

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。

class Time 
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date 
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d;

	return 0;
}

 运行结果:

对于用户定义的类型(如类、结构体),如果它们没有定义自己的构造函数,编译器会生成一个默认的无参数构造函数(也称为“合成默认构造函数”)。这个构造函数会按成员变量在类中声明的顺序调用它们的默认构造函数(如果有的话)。

代码示例:

#include <iostream>  
#include <string>  
  
class MemberClass 
{  
public:  
    MemberClass() 
    {  
        std::cout << "MemberClass default constructor called" << std::endl;  
    }  
};  
  
class MyClass 
{  
public:  
    MemberClass member; // 用户定义类型成员变量  
    // MyClass没有定义自己的构造函数,所以编译器会生成一个默认构造函数  
};  
  
int main() 
{  
    MyClass obj; // 调用MyClass的默认构造函数,它会调用MemberClass的默认构造函数  
    return 0;  
}

 在上面的代码中,当MyClass的对象obj被创建时,它的成员变量member会被初始化,这会调用MemberClass的默认构造函数。

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
		void Print()
		{
			cout << _year << "-" << _month << "-" << _day << endl;
		}
private:
	// 基本类型(内置类型)
	int _year = 2024;
	int _month = 12;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

注意:构造函数遵循先定义先构造原则。 

三、析构函数(Destructor)

1.析构函数的基本概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。

2. 析构函数的特征

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

class MyClass 
{
public:
    ~MyClass() {
        // 释放资源的代码  
        std::cout << "Destructor called!" << std::endl;
    }
};

在上面的例子中,~MyClass() 是 MyClass 类的析构函数。

3.析构函数的使用

在我们前面学习栈的实现的时候,动态开辟后的内存需要手动进行释放,但是我们很有可能会忘记释放,因为即使不释放空间,程序也可以正常运行。如果使用析构函数,那么会自动调用,能够有效减少忘记释放的可能。 

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)//全缺省默认构造函数
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc fail!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)//入栈
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()//析构函数
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}

我们在设置栈的构造函数时,定义容量 capacity 时利用缺省参数默认给个4的容量,这样用的时候默认就是4,如果不想要4可以自己传。

如此一来,就可以保证了栈被定义出来就一定被初始化,用完后会自动销毁。以后就不会有忘记调用 destroy 而导致内存泄露的*了,这里的析构函数就可以充当销毁的作用。

4.默认析构函数

关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

#include<iostream>
using namespace std;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}
// 程序运行结束后输出:~Time()

在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数? 

解答:

main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。

 5.析构顺序问题

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1)
	{
		_year = year;
	}

	~Date()
	{
		cout << "~Date()->" << _year << endl;
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
};
void func()
{
	Date d3(3);//局部对象
	static Date d4(4);//局部的静态
}
Date d5(5);//全局对象
static Date d7(7);//全局静态
Date d6(6);
static Date d8(8);

// 局部对象(后定义先析构) -》 局部的静态 -》全局对象(后定义先析构)
int main()
{
	Date d1(1);//局部对象 
	Date d2(2);
	func();

	return 0;
}

析构函数执行顺序:局部对象(后定义先析构) -》 局部的静态 -》全局对象(后定义先析构) 

6.析构函数特性

???? 如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:

对于 "内置类型" 的成员变量:不会做初始化处理。
对于 "自定义类型" 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
 而我们的析构函数也是这样的。

???? 如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:

对于 "内置类型" 的成员变量:不作处理 。
对于 "自定义类型" 的成员变量:会调用它对应的析构函数  。

四、 拷贝构造函数

在C++编程中,拷贝构造函数是一个非常重要的概念,它用于创建一个新对象,作为现有对象的副本。拷贝构造函数在对象复制、函数参数传递、返回值以及容器操作等场景中扮演着关键角色。

1.定义

拷贝构造函数是一种特殊的构造函数,它接受一个同类型的对象作为参数,并用于初始化新创建的对象。其语法如下:

类名(const 类名& 形参);
ClassName(const ClassName& other);

这里,ClassName 是类的名称,other 是传递给拷贝构造函数的参数,它是一个常量引用,以避免不必要的对象复制。

2.拷贝构造函数的特性

它也是一个特殊的成员函数,所以他符合构造函数的一些特性:

① 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。

② 拷贝构造函数的参数只有一个,并且 必须要使用引用传参!

3.拷贝构造函数的用法

对象初始化

class Date {
public:
    Date(int year = 0, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

    /* Date d2(d1); */
    Date(Date& d) 
    {         // 这里要用引用,否则就会无穷递归下去
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

private:
    int _year;
    int _month;
    int _day;
};

int main(void)
{
    Date d1(2024, 10, 9);
    Date d2(d1);          // 拷贝复制

    // 看看拷贝成功没
    d1.Print();
    d2.Print();

    return 0;
}

 运行结果:

函数返回值按值返回

class Date {
public:
    Date(int year = 0, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date& d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    void Print() {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

Date Birthday()
{
    Date time(2004, 10, 29);
    return time;// 可能会调用拷贝构造函数来返回对象的副本

}

int main()
{
    Date T = Birthday();// 拷贝构造函数被调用,用返回的对象副本初始化 T
    return 0;
}

在这个例子中,Birthday函数返回一个对象,当这个对象被返回并用于初始化 T 时,可能会调用拷贝构造函数来创建 T。 

4.拷贝构造函数的深拷贝与浅拷贝

除了上面提到的基本用法,拷贝构造函数还涉及到深拷贝和浅拷贝的概念。

浅拷贝

默认情况下,C++ 的拷贝构造函数执行的是浅拷贝。这意味着它只是简单地复制对象的成员变量值。如果成员变量是基本类型(如 intchar 等),那么浅拷贝没有问题。但如果成员变量是指针或引用类型,浅拷贝就可能会导致问题。

class ShallowPerson {
private:
    std::string* namePtr;
    int age;

public:
    ShallowPerson(std::string n, int a) 
    {
        namePtr = new std::string(n);
        age = a;
    }

    // 浅拷贝构造函数(默认情况下的拷贝构造函数行为)
    ShallowPerson(const ShallowPerson& other) 
    {
        namePtr = other.namePtr;
        age = other.age;
        std::cout << "浅拷贝构造函数被调用,可能会有问题哦!????" << std::endl;
    }

    ~ShallowPerson() 
    {
        delete namePtr;
    }

    void introduce() 
    {
        std::cout << "我叫 " << *namePtr << ",我 " << age << " 岁啦!" << std::endl;
    }
};

int main() 
{
    ShallowPerson sp1("酒鬼猿", 20);
    ShallowPerson sp2 = sp1;

    sp1.introduce();
    sp2.introduce();

    // 当其中一个对象被销毁时,可能会导致另一个对象出现问题
    return 0;
}

 

 在这个例子中,浅拷贝只是复制了指针的值,而不是复制指针所指向的内容。这就导致两个对象的指针指向了同一块内存。当其中一个对象被销毁并释放内存时,另一个对象的指针就变成了悬空指针,再使用这个指针就会导致未定义的行为。

深拷贝

为了解决浅拷贝的问题,我们需要实现深拷贝。深拷贝会复制对象的所有成员变量,包括指针所指向的内容。

class DeepPerson {
private:
    std::string* namePtr;
    int age;

public:
    DeepPerson(std::string n, int a) {
        namePtr = new std::string(n);
        age = a;
    }

    // 深拷贝构造函数
    DeepPerson(const DeepPerson& other) {
        age = other.age;
        namePtr = new std::string(*(other.namePtr)); // 复制指针所指向的内容
        std::cout << "深拷贝构造函数被调用,一切都好啦!????" << std::endl;
    }

    ~DeepPerson() {
        delete namePtr;
    }

    void introduce() {
        std::cout << "我叫 " << *namePtr << ",我 " << age << " 岁啦!" << std::endl;
    }
};


int main() {
    DeepPerson dp1("酒鬼猿", 21);
    DeepPerson dp2 = dp1;

    dp1.introduce();
    dp2.introduce();

    // 现在即使一个对象被销毁,另一个对象也不会受到影响
    return 0;
}

 

在深拷贝构造函数中,我们为新对象分配了新的内存,并将原对象指针所指向的内容复制到新内存中。这样,两个对象就拥有了各自独立的内存空间,不会相互影响。 

 5.引用传参(面试题)

面试题: 为什么必须使用引用传参呢?

解答:

当我们使用值传递的方式传递参数时,实际上是将实参复制一份传递给函数的形参。这意味着会创建一个新的对象,这个新对象是实参的副本。

假设我们有一个类 MyClass,如果拷贝构造函数不使用引用传参,如下所示:

class MyClass 
{
public:
    int value;
    MyClass(MyClass obj) 
    { 
        value = obj.value;
    }
};

当我们使用一个已有的对象去初始化另一个新对象时,比如这样的代码:

MyClass obj1;
obj1.value = 10;
MyClass obj2(obj1);

在这个过程中,会调用拷贝构造函数来初始化 obj2。但是,由于拷贝构造函数的参数是按值传递的,所以在调用拷贝构造函数时,会将 obj1 复制一份传递给拷贝构造函数的形参 obj。而这个复制的过程又会触发拷贝构造函数的调用,因为要创建形参 obj 的副本。这样就形成了一个无限递归的过程。

具体来说,当执行 MyClass obj2(obj1); 时:

  1. 首先尝试调用拷贝构造函数来创建 obj2,将 obj1 作为实参传递给拷贝构造函数。
  2. 由于拷贝构造函数是按值传递参数,所以要创建形参 obj 的副本,这就需要再次调用拷贝构造函数。
  3. 第二次调用拷贝构造函数时,又要创建新的形参副本,再次触发拷贝构造函数的调用。
  4. 这个过程会不断重复下去,直到程序耗尽栈空间而崩溃。

为了避免这种无限递归的情况,拷贝构造函数的参数应该使用引用类型,当使用引用传参时,形参只是实参的一个别名,不会创建新的对象副本。这样在调用拷贝构造函数时,就可以直接使用传入的对象进行初始化,而不会触发新的拷贝构造函数调用,从而避免了无限递归的问题。

本篇博客到此结束,如果大家浏览后发现什么错误之处,或者有什么问题,评论区留言哦~

上一篇:数据库血缘工具学习,使用以及分享


下一篇:【选择C++游戏开发技术】