音视频系列--c++语言学习(构造函数、拷贝构造函数、explicit、new、内存分布)

一、构造函数


1.1、构造函数和析构函数由来

类的数据成员不能在类的声明时候初始化

使用构造函数处理对对象的初始化。

1 .构造函数是一种特殊的成员函数,与其他函数不同,不需要用户调用它,而是创建对象的时候自动调用。
2 .析构函数是对象不再使用的时候,需要清理资源的时候调用。

1.2、构造函数和析构函数基本语法

1.2.1、构造函数

1.C++中的类需要定义与类名相同的特殊成员函数时,这种与类名相同的成员函数叫做构造函数;
2.构造函数可以在定义的时候有参数;
3.构造函数没有任何返回类型。
4.构造函数的调用: 一般情况下,C++编译器会自动的调用构造函数。特殊情况下,需要手工的调用构造函数。

class Test {
public:
    //构造函数
    Test() {

    }
} 
1.2.2、析构函数

1.C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的函数是析构函数;
2.析构函数没有参数和没有任何返回类型;
3.析构函数在对象销毁的时候自动调用;
4.析构函数调用机制: C++编译器自动调用。

class Test {
    ~Test() {

    }
}

1.3、编译器构造析构方案 PK 对象显示初始化方案

#include <iostream>

using namespace std;

class Test {
private:
    int x;
public:
    Test(int x) {
        this->x = x;
        cout << "对象被创建" << endl;
    }

    Test() {
        x = 0;
        cout << "对象被创建" << endl;
    }

    void init(int x) {
        this->x = x;
        cout << "对象被创建" << endl;
    }

    ~Test() {
        cout << "对象被释放" << endl;
    }

    int GetX() {
        return x;
    }
};

int main() {
    //1.我们按照C++编译器提供的初始化对象和显示的初始化对象
    Test a(10);
    Test b;      //显示创建对象但是还是调用无参构造函数,之后显示调用了初始化函数
    b.init(10);

    //创建对象数组(使用构造函数)
    Test arr[3] = {Test(10), Test(), Test()};
    //创建对象数组 (使用显示的初始化函数)
    Test brr[3];   //创建了3个对象,默认值
    cout << brr[0].GetX() << endl;
    brr[0].init(10);
    cout << brr[0].GetX() << endl;
    return 0;
} 

根据上面的代码, 我们得出使用显示的初始化方案

1 .必须为每个类提供一个public的init函数;
2 . 对象创建后必须立即调用init函数进行初始化。

优缺点分析:

1 . init函数只是一个普通的函数,必须显示的调用;
2 . 一旦由于失误的原因,对象没有初始化,那么结果也是不确定的。没有初始化的对象,其内部成员变量的值是不定的;
3 . 不能完全解决问题。

1.4、构造函数的分类

1.4.1、无参构造函数
class Test {
private:
    int x;
public:
    Test() {
        this->x = 10;
    }
};

无参构造函数的调用: Test a;

1.4.2、有参构造函数
class Test {
private:
    int x;
public:
    Test(int x) {
        this->x = x;
    }
} 

有参数构造函数的创建方式:

1 .Test a(10); 调用有参数构造函数
2 .Test b=(2,3); 逗号表达式的值是最后一位,调用有参数构造函数
3 .Test c=Test(2); 产生一个匿名对象,直接转化成c(只会调用一次有参数构造函数)

1.4.3、拷贝构造函数

使用对象a初始化对象b

class Test
{
   private:
          int x;
   public:
          Test(const Test& a)
          {
             this->x=a.x;
          }
} 

拷贝构造函数的调用时机

class Test
{
private:
    int x;
public:
    Test(int x)
    {
        this->x = x;
        cout << "对象被创建" << endl;
    }
    Test()
    {
        x = 0;
        cout << "对象被创建" << endl;
    }
    ~Test()
    {
        cout << "对象被释放" << endl;
    }
    Test(const Test& a)
    {
        this->x = a.x;
        cout << "对象被创建(拷贝构造函数)" << endl;
    }
}; 

1 .第一个场景: 用对象a初始化对象b

Test a(10);
//调用的是拷贝构造函数
Test b = a; 

2 .第二个场景: 用对象a初始化对象b

Test a(10);
//调用的是拷贝构造函数
Test b(a) 

第一个场景和第二个场景是一样的,用一个对象初始化另一个对象。

3 .第三个场景: 实参初始化形参的时候,会调用拷贝构造函数

Test a(10);
//实参初始化形参
PrintF(a); 

4 .第四个场景: 函数返回一个匿名对象

Test p()
{
    Test c(4);
    return c;
} 

匿名对象去和留总结:
1 . 匿名对象初始化另一个对象时,匿名对象直接变成有名的对象(初始化的那个对象);
2 . 匿名对象赋值另一个对象时,匿名对象赋值成功会被析构。

1.5、默认的构造函数

两个特殊的构造函数:
1 . 默认的无参数构造函数: 当类中没有定义构造函数时,编译器会提供一个无参数构造函数,并且函数体为空;
2 . 默认的拷贝构造函数: 当类中没有定义拷贝构造函数时,编译器会提供一个默认的拷贝构造函数,简单的进行成员变量的值复制。

1.6、构造函数调用规则

1 . 当类中没有定义一个构造函数的时候,C++编译器会提供默认的无参数构造函数和拷贝构造函数;
2 . 当类中定义了拷贝构造函数,C++编译器不会提供无参数构造函数;
3 . 当类中定义了任意的非拷贝构造函数,C++编译器不会提供默认的无参数构造函数;
4. 默认的拷贝构造函数只是进行成员变量的简单赋值;

1.7、构造函数和析构函数的总结

1 . 构造函数时C++中用于初始化对象状态的特殊函数;
2 . 构造函数在对象创建的时候自动调用;
3 . 构造函数和普通成员函数都遵循重载原则;
4 . 拷贝构造函数是对象正确初始化的重要保障;
5 . 必要的时候必须手工的写拷贝构造函数。

二、拷贝构造函数


2.1、什么是拷贝构造函数

首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a = 100;
int b = a; 

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。 下面看一个类对象拷贝的简单例子。

class CExample {
private:
     int a;
public:
      //构造函数
     CExample(int b)
     { a = b;}

      //一般函数
     void Show ()
     {
        cout<<a<<endl;
      }
};

int main()
{
     CExample A(100);
     CExample B = A; //注意这里的对象初始化要调用拷贝构造函数,而非赋值
      B.Show ();
     return 0;
}

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

下面举例说明拷贝构造函数的工作过程。

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种 特殊的构造函数 ,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。

2.2、浅拷贝和深拷贝

2.2.1、默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值。

2.2.2、浅拷贝

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。

double free问题

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

class Person{
public:
    int age;
    char *name;
public:
    Person(){
        cout << "new person " << endl;
    }
    Person(char *name ,int age){
        cout << "new person int a, int b" << endl;
        this->age = age;
        this->name = name;
    }

    Person(const Person  &person){
        cout << "const Person  int a" << endl;
        this->age = person.age;
        this->name = person.name;
    }

    ~Person(){
        cout << "delete  person " <<this->age<< endl;
        if (this->name != NULL) {
            free(name);
            name = NULL;
        }
    }

};

int main() {
    Person p("david", 12);
    cout << "name: "<<p.name<<" age :" <<p.age<< endl;

    Person p2(p);

    return 0;
}

在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,会造成报错,解决办法就是使用“深拷贝”。

2.2.3、深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。

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

class Person{
public:
    int age;
    char *name;
public:
    Person(){
        cout << "new person " << endl;
    }
    Person(char *name ,int age){
        cout << "new person int a, int b" << endl;
        this->age = age;
        this->name = (char *) malloc(strlen(name)+1);
        strcpy(this->name, name);
    }

    Person(const Person  &person){
        cout << "const Person  int a" << endl;
        this->age = person.age;
        this->name = (char *) malloc(strlen(person.name)+1);
        strcpy(this->name, person.name);
    }

    ~Person(){
        cout << "delete  person " <<this->age<< endl;
        if (this->name != NULL) {
            free(name);
            name = NULL;
        }
    }

};

int main() {
    Person p("david", 12);
    cout << "name: "<<p.name<<" age :" <<p.age<< endl;

    Person p2(p);
    cout << "name: "<<p2.name<<" age :" <<p2.age<< endl;

    return 0;
}
2.2.4、防止默认拷贝发生

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数*。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

2.2.5、以下函数哪个是拷贝构造函数

对于一个类X, 如果一个构造函数的第一个参数是下列之一:

X&
const X&
volatile X&
const volatile X&

且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

X::X(const X&);  //是拷贝构造函数    
X::X(X&, int=1); //是拷贝构造函数   
X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数

一个类中可以存在多于一个的拷贝构造函数吗? 解答:类中可以存在超过一个拷贝构造函数。

class X { 
public:       
  X(const X&);      // const 的拷贝构造
  X(X&);            // 非const的拷贝构造
};

注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.

class X {    
public:
  X();    
  X(X&);
};    

const X cx;    
X x = cx;    // error

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。 这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

三、explicit关键字


在C++中,我们有时可以将构造函数用作自动类型转换函数。但这种自动特性并非总是合乎要求的,有时会导致意外的类型转换,因此,C++新增了关键字explicit,用于关闭这种自动特性。即被explicit关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换。

注意:只有一个参数的构造函数,或者构造函数有n个参数,但有n-1个参数提供了默认值,这样的情况才能进行类型转换。

下面通过一段代码演示具体应用(无explicit情形):

 /* 示例代码1 */
 class Demo
 {
    public:
     Demo();                     /* 构造函数1 */
     Demo(double a);              /* 示例代码2 */
     Demo(int a,double b);           /* 示例代码3 */
     Demo(int a,int b=10,double c=1.6);  /* 示例代码4 */
     ~Demo();
     void Func(void);
 
    private:
     int value1;
     int value2;
 };

上述四种构造函数:

构造函数1没有参数,无法进行类型转换!

构造函数2有一个参数,可以进行类型转换,如:Demo test; test = 12.2;这样的调用就相当于把12.2隐式转换为Demo类型。

构造函数3有两个参数,且无默认值,故无法使用类型转换!

构造函数4有3个参数,其中两个参数有默认值,故可以进行隐式转换,如:Demo test;test = 10; 。

下面讲述使用了关键字explicit的情况:

 /* 示例代码2 */
 class Demo
{
   public:
    Demo();                     /* 构造函数1 */
    explicit Demo(double a);        /* 示例代码2 */
    Demo(int a,double b);           /* 示例代码3 */
  
     ~Demo();
     void Func(void);
    private:
     int value1;
     int value2;
};

在上述构造函数2中,由于使用了explicit关键字,则无法进行隐式转换。即:Demo test;test = 12.2;是无效的!但是我们可以进行显示类型转换,如:

Demo test;

test = Demo(12.2); //或者

test = (Demo)12.2;

四、new 关键字


4.1、C中使用malloc出现的问题

1 .程序员必须确定对象的长度
2 .malloc 返回一个(void *)指针 ,c++不允许将(void*) 赋值给其它指针,必须强转
3 .malloc可能申请内存失败,所以必须判断返回值来保存内存分配成功
4 .用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用的),用户有可能忘记调用初始化函数

c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new和delete

4.2、new运算符和delete运算符

1 .Person *p=new Person会返回一个Person
2 .默认调用析构函数,开辟空间,返回不是void*,不需要强制转换
3 .delete释放
4 .new对象用void *去接受,释放不了对象
5 .new出来的是数组,如何释放 ? delete[]
6 .new出来的是数组,肯定会调用默认构造

#include<iostream>

using namespace std;

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

	}
	Person(int a)
	{
		cout << "有参构造调用" << endl;
	}


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

};

void test01()
{
	//Person p1; //栈区开辟

	Person *p2 = new Person;//堆区开辟
	//所有new出来的对象,都会返回该类型的指针

	//malloc返回void*还要强转
	//malloc会调用构造吗?不会 new会调用构造
	//new运算符,malloc是函数
	//释放堆区域的空间

	//delete也是运算符,要配合new用,malloc配合free用
	delete p2;
}
void test02()
{
	void *p = new Person;
	//当用void* 接受new出来的指针,会出现释放的问题
	delete p;
	//无法释放p
}

void test03()
{
	//同过new来开辟数组
	//一定会调用默认构造函数,所以一定要提供默认构造
	Person *pArray = new Person[10];

	//Person pArray2[2] = { Person(1), Person(2) };//在栈上开辟数组,可以指定有参构造

	//释放数组 delete[]必须加上中括号
	delete []pArray;
}


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

4.3 malloc/free和new/delete的区别

malloc/free和new/delete的

共同点是:

都是从堆上申请空间,并且需要用户手动释放。

不同的地方是:

1 .malloc和free是函数,new和delete是操作符
2 .malloc申请的空间不会初始化,new可以初始化
3 .malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
4 .malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5 .malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6 .申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间 后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
7 .new/delete比malloc和free的效率稍微低点,因为new/delete的底层封装了malloc/free

五、内存分布


对于分段式内存而言,C++程序所涉及的内存区有:代码段、数据段、堆栈。(见图)

img
下面来依次分析C++程序中各个元素所对应的内存位置吧~

程序代码:存储在代码段,只读。

变量:

根据作用域,可分为:全局变量、局部变量

根据关键字,有:static静态变量、const常量

根据位置,有:类变量、函数形参

特殊变量:字面量

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在引用的内容中,并没有提到explicit关键字可以直接修饰拷贝构造函数explicit关键字通常用于修饰类的构造函数,以防止隐式类型转换。在C++中,拷贝构造函数是一个特殊的构造函数,用于创建对象的副本。通常情况下,拷贝构造函数不会被explicit关键字修饰。所以,如果要强制禁止隐式调用拷贝构造函数,可以使用其他的方法,例如将拷贝构造函数声明为私有或删除该函数。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [explicit作用,拷贝构造函数,隐式类型](https://blog.csdn.net/NBE999/article/details/77881518)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [【C++学习explicit修饰构造函数](https://blog.csdn.net/TwT520Ly/article/details/80974757)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [c/c++拷贝构造函数和关键字explicit详解](https://download.csdn.net/download/weixin_38548589/13994418)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值