C++学习笔记(4)

目录

一.构造函数

1.为什么有构造函数

2.委托构造

3.拷贝构造函数

4.C++结构体中的构造函数 

二.析构函数

1.析构函数是啥

2.析构函数长啥样

三.深拷贝和浅拷贝

四.构造和析构的顺序问题

五.实现简单的string类创建和其他功能函数


一.构造函数

1.为什么有构造函数

在类中,构造函数,顾名思义,就是一个用于构造对象的函数。那构造函数怎么写?它跟前面讲过的普通的创建对象有啥不一样呢?下面一一来解释。首先是如何写构造函数:

简单来说,构造函数看起来就是:函数名与类名相同,并且没有返回值的函数。注意:很多人会误以为 “void” 是没有返回值的意思,其实不是的,void的意思是 “无类型” ,不是为空。

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

	}
protected:
	string name;
	int age;
};

前面说,构造函数是用于构造对象的,那他是怎么实现的?具体如下:

class A
{
public:
	A(string m_name,int m_age) 
	{
		age = m_age;
		name = m_name;
		cout << "我调用了构造函数"<<endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};
int main()
{
	A a("小蓝",18);
	a.print();
}

 如图,这里可以很清楚地展示出这些函数的调用过程,可以看到在主函数中,我们创建了一个对象 “a” ,接着再调用函数打印了名字和年龄,由打印的结果来看函数执行的顺序是  构造函数->打印函数  ,所以:构造函数的执行是在完成创建对象的时候执行的,这也是它的意义所在。有些人可能不理解,不是说创建对象的时候会执行构造函数吗?为什么之前没写构造函数的时候也能创建对象?这其实是因为:如果不写构造函数,在任意一个类中,都会存在一个默认的无参的构造函数。因此,其实不是没有调用,只是这个函数无参且无返回值,你感受不到罢了。

还有一个重要的知识点,那就是:函数的缺省和重载对构造函数同样适用!这点很重要!

class A
{
public:
	A(string m_name = "小绿",int m_age = 19)
	{
		age = m_age;
		name = m_name;
		cout << "我调用了构造函数"<<endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};
int main()
{
	A a("小蓝",18);
	A b("小红");
	A c;
	a.print();
	b.print();
	c.print();
}

可以看到,函数的缺省对构造函数仍然适用!而且它的不填初始化参数时调用的默认构造函数是全部参数都缺省的那一个,而不是原来那个什么都没有的函数。

那有些比较 “刁钻” 的可能就会问:那我想要原来那个 “纯纯的” 构造函数怎么办,我不想要这么多参数,那你可以这样实现:

A()
{
		
}
//也可以 : A() = default;
A(string m_name ,int m_age = 19)
{
	age = m_age;
	name = m_name;
	cout << "我调用了构造函数"<<endl;
}

 所以简单总结一下,构造一个对象的过程,就是调用这个函数的过程。

2.委托构造

委托构造,简单来说,就是用构造函数调用另一个构造函数,从而达到为默认的构造函数赋值的目的,只要采用原构造函数的形式,就没有问题。

而要使用委托构造的方式,就只能用初始化参数列表的写法,那什么是初始化参数列表?

构造函数名(参数1,参数2,...):成员1(参数1),成员2(参数2)...{}

 按这种形式写就是初始化参数列表,成员也可以不用按第一个第二个的顺序对应,但是一般在写的时候会对应起来,这样不容易写错!而且初始化参数列表有个很实用的好处就是可以避免同名问题,比如:

class A
{
public:
	A(string name , int age):name(name),age(age)
	{
		cout << "我调用了构造函数"<<endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};

可以看到,我定义了参数name,但是我在构造函数形参那里也写了一个name, 这样不会导致同名重复,因为在初始化参数列表中,它会默认括号外的是类里面真正存的,括号里面的是初始化传进去的,两个在我们眼里长得一样,但在计算机眼里可完全不一样哦!

那又有人要问了,既然括号里面的是传进去的,那我传自己定义好的外部变量可以吗?当然可以!

string Myname = "小蓝";
class A
{
public:
	A( int age):name(Myname),age(age)
	{
		cout << "我调用了构造函数"<<endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};

好了,讲完了初始化参数列表,那我们来看委托构造! 

A(string m_name , int m_age = 19)
{
	age = m_age;
	name = m_name;
	cout << "我调用了构造函数"<<endl;
}
A():A("小蓝",20){}

这样一来,构造函数的默认值年龄就是20,可以不受函数缺省的约束。默认的构造函数本身没有初始化的能力,它是依托别的函数来完成初始化,这种方式叫做委托构造。

3.拷贝构造函数

拷贝构造函数长相上跟构造函数基本上是一样的,所以实际上拷贝构造函数就是构造函数的一种。唯一的区别就是传递的参数是唯一的,是对对象的引用。

A(A& a)
{
	name = a.name;
	age = a.age;
}

拷贝构造函数能从对象a身上复制一份一模一样的拷贝本给对象b,来分析这段有意思的代码:

class A
{
public:
	A(string name, int age):name(name),age(age)
	{
		cout << "我调用了构造函数"<<endl;
	}
	A()
	{
		cout << "我调用了构造函数" << endl;
	}
	A(A& a)
	{
		name = a.name;
		age = a.age;
		cout << "我调用了拷贝构造函数" << endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};
int main()
{
	A a("小蓝", 18);
	A b(a);   //拷贝构造函数的显式调用
	A c = a;  //拷贝构造函数的隐式调用
	A d;
	d = a;    //注意,这里没有调用拷贝构造函数,这里是运算符重载的效果,后面会涉及到这块内容
	a.print();
	b.print();
	c.print();
	d.print();
}

一起来分析它的输出吧!主函数第一句话,构造一个对象,所以调用构造函数,输出 “我调用了构造函数” ,下一句是用拷贝构造函数的方式构造b,这是显式调用的方式,所以输出 “我调用了拷贝构造函数” ,接下来是用拷贝构造函数的方式构造c,这是隐式调用的方式,所以输出 “我调用了拷贝构造函数” ,下一句是构造了一个对象d,这里没有任何说明,所以这是默认的构造函数语句,所以输出 “我调用了构造函数” ,至此,构造已经全部完成,接下来的赋值是运算符重载的操作,跟构造函数没有关系。然后就输出四组相同的名字以及年龄。

 在第一次笔记的时候说过,所有的函数传参其实就是赋值拷贝,可以看下面的代码:

class A
{
public:
	A(string name, int age):name(name),age(age)
	{
		cout << "我调用了构造函数"<<endl;
	}
	A()
	{
		cout << "我调用了构造函数" << endl;
	}
	A(A& a)
	{
		name = a.name;
		age = a.age;
		cout << "我调用了拷贝构造函数" << endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	string name;
	int age;
};
void printData1(A a)
{
	a.print();
}
void printData2(A& a)
{
	a.print();
}
int main()
{
	A a("小蓝", 18);
	printData1(a);
}

我们定义了两个功能相同但传参不同的函数,先来执行第一个,看看有什么效果?

可以看到,在传参的过程中,使用了拷贝的方式,即把a传进函数时,调用这个函数生成了一个临时的对象,再把a赋值给临时的对象,这个临时的对象在函数里作用着,此后跟原来的a是完完全全不同的两个对象。

再来看看传引用的函数吧!

printData2(a);

可以看到,此时传进去的不是拷贝本啦,可以理解为实实在在的对象啦!(前面的坑填上了!)

此外,写了拷贝构造函数的类还能用来创建匿名对象,需要注意的是,匿名对象十分特殊。本质其实是一个右值,因此通过匿名对象给其他对象初始化时,拷贝构造函数要加const修饰,而实际上,匿名对象是创建一个临时对象,然后通过赋值的方式将对象的所有权转交给想要初始化的那个对象,因此,如果我们定义了一个匿名对象但没有通过它给其他对象赋值,即没有转交所有权,那这个匿名对象会立刻死亡。(意思就是它是不存在的,不能通过它调用函数什么的)不多说,上代码!

//类中
A(const A& a)
{
	name = a.name;
	age = a.age;
	cout << "我调用了拷贝构造函数" << endl;
}
//主函数中
A a = A("小蓝", 18);

4.C++结构体中的构造函数 

之前说过C++结构体跟C语言的区别,详情可看这篇:C++学习笔记(2)。C++的结构体在不写构造函数前,就可以用C语言的结构体用法,比如在初始化的时候直接赋值等。但写了构造函数之后,它就完完全全变成了一个默认属性为共有属性的类。当你采用创建时赋值的时候,也会调用构造函数,因此,想要在创建的时候赋值,要么不写构造函数,要么写一个能赋值的构造函数,就这么简单。

二.析构函数

1.析构函数是啥

析构,有释放,拆解结构的意思。当类中的数据成员是指针,并且进行了动态内存申请时,我们就需要写析构函数来释放内存。同样的,如果我们不写析构函数,类中也会有一个默认的析构函数,在我们对象死亡的时候会调用它,这点跟构造函数很像,一个是创建时调用,一个是死亡时调用。

2.析构函数长啥样

它是一个无参数,无返回值,函数名是类名前加一个 “~” 的函数,具体长这样:

~A()
{
	cout << "我是析构函数" << endl;
}

现在在之前写的构造函数的基础上我们加上这个析构函数,然后在主函数中创建一个对象:

int main()
{
	{
		A a("小蓝", 18);
		a.print();
	}
	cout << "我出了大括号咯!" << endl;

}

我们来分析一下会输出什么?首先,我们创建了一个对象,所以输出了 “我调用了构造函数” ,接下来打印个人信息 “小蓝 18” ,然后重点来了!我们上面说过,在对象死亡的时候会调用析构函数,而在这里对象的生存域就是它外面这一层大括号,下一句就要出括号了,所以它会先输出 “我是析构函数” ,再打印 “我出了大括号咯!”。

可以看到打印结果跟我们的分析是一致的!

那我们回到析构函数的具体应用,我们来看下面这些代码:

//类中
A(const char* pname, int age) :age(age)
{
	name = new char[strlen(pname) + 1];
	strcpy_s(name, strlen(pname) + 1, pname);
}
~A()
{
	delete[] name;
}
//主函数中
int main()
{
	A a("小蓝", 18);
}

可以看到,我们在类中定义了一个指针并且为它申请了一块内存,那么我们在析构函数中就得写一个释放内存的语句,这样能有效防止内存泄漏。再来看一段有意思的代码:

class A
{
public:
	A(const char* pname, int age) :age(age)
	{
		name = new char[strlen(pname) + 1];
		strcpy_s(name, strlen(pname) + 1, pname);
	}
	A() = default;
	~A()
	{
		delete[] name;
		cout << "析构函数" << endl;
	}
	void print()
	{
		cout << name << " " << age << endl;
	}
protected:
	char* name;
	int age;
};
int main()
{
	A* b = new A("小蓝",18);
	b->print();
}

此时它会输出什么呢?

我们会发现,此时它并没有调用析构函数。因为在这里这个对象是存在自由存储区的,我们创建的是一个指向它的指针,不是一个对象,而自由存储区的内存需要我们自己手动释放。因此,要想使用对象时要额外小心,可能你以为你能调用析构函数来释放内存,不会导致内存泄漏,实际上你是通过指针new了一个对象,它并不会调用构造函数,而是要自己手动释放。因此,在这段代码上加入 “delete b” 即可!

三.深拷贝和浅拷贝

关于深浅拷贝,主要是要理解传参的真正内涵——拷贝赋值和引用,前面的文章也有多次提及,如果理解了传参,那再来理解这个就不难!

我们先来看何为浅拷贝?默认的拷贝构造均为浅拷贝,使用拷贝赋值传参的方式构造的对象也是浅拷贝,而浅拷贝会发生一个致命的问题就是:析构问题。简单来说就是内存重复释放问题,这么说肯定是一头雾水,我们来看实例分析:

class A
{
public:
	A(const char* pname, int age) :age(age)
	{
		name = new char[strlen(pname) + 1];
		strcpy_s(name, strlen(pname) + 1, pname);
	}
	A() = default;
	~A()
	{
		delete[] name;
	}
protected:
	char* name;
	int age;
};
int main()
{
	A a("小蓝", 18);
	A b(a);
	A c = a;
}

从表面上看,我们在主函数中创建了一个对象,然后通过这个对象用默认的拷贝构造函数对其进行拷贝赋值,这种就是浅拷贝,因为这里的赋值不是真正意义上给了b和c一个崭新的对象,它里面还残留着a的东西,我们看到,构造函数中传进去的是一个指针,指针的作用就是指向一块内存,当它把字符串指针传进去之后,b和c并没有生成自己专属的name,他们用的都是a的name,因为传的是一个指针,并没有重新new一个name指向想要的地方。所以,当三个对象出了作用域相继死亡时,会各自调用析构函数,析构函数中会释放这个指针指向的内存,而我们可以发现,指针指向的均是同一块内存,因此程序就会出错!

那正确的我们应该怎么做呢?

class A
{
public:
	A(const A& a)
	{
		name = new char[strlen(a.name) + 1];
		strcpy_s(name, strlen(a.name) + 1, a.name);
		age = a.age;
	}
	A() = default;
	A(const char* pname , int age = 18) : age(age) 
	{
		name = new char[strlen(pname) + 1];
		strcpy_s(name, strlen(pname) + 1, pname);
	}
	~A()
	{
		delete[] name;
	}
protected:
	char* name;
	int age;
};
int main()
{
	A a("小蓝",18);
	A b(a);
	A c = a;
}

可以看到,当我们重新写了一个拷贝构造函数时,在拷贝时重新做了动态内存申请并且通过strcpy拷贝赋值,真真正正开辟了一块属于b和c的空间,所以,当他们依次死亡时,不会重复释放同一块内存。

四.构造和析构的顺序问题

讲完了构造函数和析构函数的概念,那对于这个顺序的判断应该八九不离十了,但有一些比较特殊的顺序还是值得总结的,具体我们来分析下面的代码:

class A
{
public:
	A(string name = "x"):name(name)
	{
		cout << name;
	}
	~A()
	{
		cout << name;
	}
protected:
	string name;
};
int main()
{
	A a("a");
	static A b("b");
	A* c = new A("c");
	A d[4];
	delete c;
	c = nullptr;
}

如果能靠着自己的理解分析出来打印结果的话,那说明理解到位了,分析不出来也没关系,一起来看看吧!

主函数中先是创建了许多对象,因此最先输出的是构造函数里的语句,逐句输出abc,到d这里,因为没有对其赋值,且这是一个对象数组,里面有四个对象,因此输出默认的xxxx;到这里构造函数的输出完了,接下来就是析构函数,这几个对象,谁先析构呢?答案是我们手动析构的c,delete能直接调用对象的析构函数,剩下的就是按照 “先构造后析构” 的顺序,但这里还有个例外,那就是static关键字,它把对象存在了静态存储区,使得它的生命周期延长了,程序关闭时才会死亡,所以b是最后析构的,因此按照分析,我们输出的顺序应该是:abcxxxxcxxxxab。

果然是这样! 

五.实现简单的string类创建和其他功能函数

首先要实现创建一个对象,需要写无参构造函数,有参构造函数,拷贝构造函数,再写一个析构函数,就能实现创建的基本功能了。

class MyString
{
public:
	//无参初始化赋值
	MyString()
	{
		str = new char;
		*str = '\0';
		strSize = 0;
	}
	//有参构造函数
	MyString(const char* pstr)
	{
		strSize = strlen(pstr);
		str = new char[strSize + 1];
		strcpy_s(str, strSize + 1, pstr);
	}
	//拷贝构造函数
	MyString(const MyString& mystring)
	{
		strSize = mystring.strSize;
		str = new char[strSize + 1];
		strcpy_s(str, strSize + 1, mystring.str);
	}
	//析构函数
	~MyString()
	{
		delete[] str;
		str = nullptr;
		strSize = 0;
	}
protected:
	char* str;
	int strSize;
};

接着添加一些功能代码,例如append,compare,getlen,c_str等,里面用到了C语言字符串的操作,更深层的操作我有空会写一下的,这里就直接调用库函数了。

//连接MyString
MyString append(const MyString& mystring)
{
	MyString temp;
	temp.strSize = mystring.strSize + strSize;
	temp.str = new char[temp.strSize + 1];
	memset(temp.str, 0, temp.strSize + 1);
	strcat_s(temp.str, strSize+1, str);
	strcat_s(temp.str, temp.strSize+1, mystring.str);
	return temp;
}
//转为char*
char* c_str()
{
	return str;
}
char* data()
{
	return str;
}
//获取长度
int getLen()
{
	return strSize;
}
//比较函数
int compare(const MyString& mystring)
{
	return strcmp(str, mystring.str);
}

接下来放上测试代码:

int main()
{
	MyString str1 = "one";
	MyString str2(str1);
	MyString str3;
	MyString str4 = str1;
	MyString str5 = str2.append(str1);
	cout << str1.c_str() << endl;
	cout << str5.getLen() << endl;
	if (str1.compare(str5) == -1) cout << "str1小" << endl;
	else if (str1.compare(str5) == 0) cout << "一样大" << endl;
	else cout << "str1大" << endl;
	return 0;
}

编译通过,也得到了想要的效果!

今天的内容就到这里结束了!明天更新C++特殊成员的内容,敬请期待吧!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值