文章目录
- 1.构造函数初始化列表
- 1.1 初始化列表的使用
- 1.2 有参构造函数的默认值
- 2.对象所占空间大小
- 2.1 大小的计算
- 2.2 内存对齐机制
- 3. 析构函数
- 3.1 基本概念
- 3.2 总结
- 4.valgrind工具集
- 4.1 介绍
- 4.2 memcheck的使用
- 5. 拷贝构造函数
- 5.1 拷贝构造函数定义
- 5.2 浅拷贝/深拷贝
- 5.3 拷贝构造函数的调用时机
- 5.4 总结
1.构造函数初始化列表
1.1 初始化列表的使用
在构造函数体里对数据成员赋值,实际上是先调用数据成员的默认构造函数来创建对象,接着再对其进行赋值操作;而在初始化列表中初始化数据成员,是在对象创建的时候就直接完成初始化。
#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;
using std::cin;class Person
{
public:Person():_age(0),_name("张三"){cout << "我是无参构造函数" << endl;}Person(int age,string name):_age(age),_name(name){cout << "我是有参构造函数" << endl;}void print();
private:int _age;string _name;
};void Person::print()
{cout << "name=" << _name << ";age=" << _age << endl;
}int main(void)
{Person p1;p1.print();Person p2(10, "张万三");p2.print();return 0;
}
1.2 有参构造函数的默认值
构造函数的参数也可以按从右向左规则赋默认值,同样的,如果构造函数的声明和定义分开写,只用在声明或定义中的一处设置参数默认值,一般建议在声明中设置默认值。
class Person
{
public://有参构造函数参数默认值Person(int age=10 ,string name="张万一"):_age(age) ,_name(name){}void print();
private:int _age;string _name;
};void Person::print()
{cout << "name=" << _name << ";age=" << _age << endl;
}int main(void)
{Person p1(20);p1.print();return 0;
}
2.对象所占空间大小
2.1 大小的计算
成员函数并不影响对象的大小,对象的大小与数据成员有关
#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;
using std::cin;class Person
{
private:int _age;double _price;
};int main(void)
{Person p1;size_t s1 = sizeof(Person);size_t s2 = sizeof(p1);cout << s1 << ";" << s2 << endl;return 0;
}
2.2 内存对齐机制
有时一个类所占空间大小就是其数据成员类型所占大小之和,有时则不是,这就是因为有内存对齐的机制。
1.什么是内存对齐:
存对齐指的是将数据存储在特定的内存地址上,使得数据的起始地址是其大小的整数倍。例如,一个int类型(通常为 4 字节)的变量,其起始地址通常是 4 的倍数。
2.原因
(1)提高访问效率:计算机的硬件设计使得在访问对齐的数据时更加高效。因为 CPU 通常按字长(如 4 字节或 8 字节)来访问内存,如果数据是对齐的,CPU 可以一次读取完整的数据;如果数据未对齐,可能需要多次访问内存才能获取完整的数据。
(2)硬件限制:某些硬件平台要求数据必须按特定的对齐方式存储,否则会抛出异常或导致性能下降。
对齐规则
(1)基本数据类型的对齐值:每种基本数据类型都有其默认的对齐值,通常等于该类型的大小。例如,char 类型的对齐值为 1 字节,int 类型的对齐值为 4 字节,double 类型的对齐值为 8 字节。
(2)结构体或类的对齐值:结构体或类的对齐值是其成员中最大对齐值。
(3)成员的存储位置:每个成员的起始地址必须是其对齐值的整数倍。如果前一个成员的结束地址不满足下一个成员的对齐要求,会在它们之间插入填充字节。
(4)结构体或类的总大小:结构体或类的总大小必须是其对齐值的整数倍。如果不满足,会在结构体或类的末尾插入填充字节。
3. 析构函数
3.1 基本概念
作用:析构函数是一种特殊的成员函数,在对象的生命周期结束时自动调用,用于释放对象所占用的资源,比如动态分配的内存、打开的文件、网络连接等。
析构函数语法:
析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。 ~ClassName(){}
#include <iostream>
#include <cstring>// 使用命名空间 std
using namespace std;class Str {
private:char* str;
public:// 构造函数Str(const char* s) {//在堆上分配空间str = new char[strlen(s) + 1];strcpy(str, s);}// 析构函数~Str() {//堆内存的销毁if(nullptr!=str){ delete[] str;str = nullptr;cout << "析构函数" << endl;}}void printString() {cout << str << endl;}
};void test(void)
{Str sw("Hello, World!");sw.printString();// 函数结束,对象 sw 被销毁,析构函数自动调用
}int main(void)
{test();return 0;
}
3.2 总结
- 对于全局对象,整个程序结束时,自动调用全局对象的析构函数。
- 对于局部对象,在程序离开局部对象的作用域时调用对象的析构函数。
- 对于静态对象,在整个程序结束时调用析构函数。
- 对于 堆对象,在使用 delete 删除该对象时,调用析构函数。
4.valgrind工具集
4.1 介绍
1.概念
Valgrind 是一款用于内存调试、性能分析和代码剖析的工具集,在 Linux 系统中广泛使用。
2.工具集中的主要工具
(1).Memcheck:这是 Valgrind 最常用的工具之一,主要用于检测内存错误。它可以检测出诸如内存泄漏、非法内存访问(如越界访问、使用未初始化的内存等)等问题。在开发过程中,这些内存错误可能会导致程序出现难以调试的崩溃或错误行为,Memcheck 能够帮助开发者快速定位和解决这些问题。
(2)Callgrind:用于性能分析,它可以收集程序中函数调用的信息,包括函数的执行时间、调用次数等。通过分析这些数据,开发者可以找出程序中的性能瓶颈所在,从而有针对性地进行优化。
(3)Cachegrind:主要关注程序的缓存行为。它可以模拟 CPU 缓存,分析程序对缓存的使用情况,帮助开发者了解缓存命中率、缓存缺失等信息,以便优化程序的内存访问模式,提高缓存利用率,从而提升程序性能。
(4)Helgrind:用于检测多线程程序中的数据竞争问题。在多线程环境下,当多个线程同时访问共享数据且没有适当的同步机制时,就可能发生数据竞争,导致程序出现不确定的行为。Helgrind 能够检测出这些潜在的数据竞争情况,帮助开发者确保多线程程序的正确性和稳定性。
3.特点
(1)强大的检测能力:能够深入检测各种内存相关的问题和性能瓶颈,为开发者提供详细的错误信息和性能数据。
(2)易于使用:Valgrind 的命令行界面相对简单,只需指定要运行的程序以及相应的工具选项,就可以开始对程序进行分析。
(3)跨平台性:支持多种 Linux 平台,具有较好的移植性,方便在不同的系统环境中使用。
4.应用场景
(1)软件开发与调试:在开发过程中,及时发现和解决内存错误可以提高软件的稳定性和可靠性,减少后期维护成本。
(2)性能优化:通过分析性能数据和缓存行为,开发者可以采取针对性的优化措施,如优化算法、调整数据结构、改进内存访问模式等,以提高程序的运行效率。
(3)多线程程序开发:确保多线程程序的正确性和稳定性,避免数据竞争等问题导致的程序错误和异常。
4.2 memcheck的使用
sudo apt-get install valgrind //安装//使用
valgrind --tool=memcheck ./a.out (简略)
valgrind --tool=memcheck --leak-check=full ./a.out (详细)//在home目录下编辑.bashrc文件,改别名;方便使用vim ~/.bashrc #打开并添加一下内存
alias memcheck='valgrind --tool=memcheck --leak-check=full --show-reachable=yes'
#保存退出 :wq
#生效
source ~/.bashrc
5. 拷贝构造函数
5.1 拷贝构造函数定义
1.语法格式
class ClassName {
public:// 拷贝构造函数ClassName(const ClassName& other) {// 初始化新对象的成员变量}
};
2.定义
该函数用一个已经存在的同类型的对象,来初始化新对象,即对对象本身进行复制 。在没有显式定义拷贝构造函数时,编译器自动提供了默认的拷贝构造函数。
#include <iostream>
#include <cstring>namespace myspace1
{using std::endl;using std::cout;using std::cin;using std::string;
}using namespace myspace1;class Person
{
public://构造函数Person():_x(0),_y(0) {cout << "我是无参构造函数" << endl;}Person(int x, int y) :_x(x), _y(y){cout << "我是有参构造函数" << endl;}//析构函数~Person(){cout << "我是析构函数" << endl;}//拷贝构造函数Person(const Person& r):_x(r._x),_y(r._y){cout << "我是拷贝构造函数" << endl;}void print(){cout << "x=" << _x << ";y=" << _y << endl;}private:int _x;int _y;
};void test()
{Person p1(3, 4);//这里调用拷贝构造函数Person p2 = p1; //==Person p2(p1)p1.print();p2.print();
}int main(void)
{test();
}
5.2 浅拷贝/深拷贝
浅拷贝:默认拷贝构造函数执行的是浅拷贝,只复制对象的成员变量的值。对于包含指针成员的对象,浅拷贝会导致多个对象的指针成员指向同一块内存区域,当其中一个对象被销毁时,释放该内存会导致其他对象的指针成为悬空指针,引发未定义行为。
深拷贝:为了避免浅拷贝带来的问题,需要显式定义拷贝构造函数,实现深拷贝。深拷贝会为新对象的指针成员分配新的内存空间,并将原对象指针所指向的内容复制到新的内存中。
错误案例
#include <iostream>
#include <cstring>namespace myspace1
{using std::endl;using std::cout;using std::cin;using std::string;
}using namespace myspace1;class Person
{
public://构造函数Person():_x(0),_name(new char[strlen("张三")+1]()){strcpy(_name, "张三");cout << "我是无参构造函数" << endl;}Person(int x, const char *name) :_x(x), _name(new char[strlen(name) + 1]()){strcpy(_name, name);cout << "我是有参构造函数" << endl;}//析构函数~Person(){delete[] _name;cout << "我是析构函数" << endl;}void print(){cout << "x=" << _x << ";name=" << _name << endl;}private:int _x;char* _name;
};void test()
{Person p1(20, "张万三");//这里调用拷贝构造函数Person p2 = p1; //==Person p2(p1)p1.print();p2.print();
}int main(void)
{test();
}
如果是默认的拷贝构造函数,p2会对p1的_name进行浅拷贝,指向同一片内存;p2被销毁时,会调用析构函数,将这片堆空间进行回收;p1再销毁时,析构函数中又会试图回收这片空间,出现double free问题
所以需要我们显示定义拷贝构造函数
#include <iostream>
#include <cstring>namespace myspace1
{using std::endl;using std::cout;using std::cin;using std::string;
}using namespace myspace1;class Person
{
public://构造函数Person():_x(0),_name(new char[strlen("张三")+1]()){strcpy(_name, "张三");cout << "我是无参构造函数" << endl;}Person(int x, const char *name) :_x(x), _name(new char[strlen(name) + 1]()){strcpy(_name, name);cout << "我是有参构造函数" << endl;}//析构函数~Person(){delete[] _name;cout << "我是析构函数" << endl;}//拷贝构造函数Person(const Person& r):_x(r._x), _name(new char[strlen(r._name) + 1]){strcpy(_name, r._name);cout << "我是拷贝构造函数" << endl;}void print(){cout << "x=" << _x << ";name=" << _name << endl;}private:int _x;char* _name;
};void test()
{Person p1(20, "张万三");//这里调用拷贝构造函数Person p2 = p1; //==Person p2(p1)p1.print();p2.print();
}int main(void)
{test();
}
所以,如果拷贝构造函数需要显式写出时(该类有指针成员申请堆空间),在自定义的拷贝构造函数中要换成深拷贝的方式,先申请空间,再复制内容
5.3 拷贝构造函数的调用时机
1.当使用一个已经存在的对象初始化另一个同类型的新对象时
void test()
{Person p1(20, "张万三");Person p2 = p1; //==Person p2(p1)p1.print();p2.print();
}
2.当函数参数(实参和形参的类型都是对象),形参与实参结合时(实参初始化形参);
void fun(Person p) //避免多余的复制,节省空间:fun(Person &p) 使用引用
{p.print();
}void test()
{Person p1(20, "张三");fun(p1);
}
3.当函数的返回值是对象,执行return语句时
//避免多余的复制,节省空间
//Person &fun1()
//{
// //返回的是引用,要注意返回对象的声明周期(返回全局变量/变量前加 static)
// return
//}
Person fun2()
{//Person(20, "李四"); 匿名对象;在这行代码执行完毕后,该临时对象就会立即调用析构函数销毁。Person p3(30, "王强");return p3; //发生复制
}
5.4 总结
1.默认情况下,c++编译器至少为我们写的类增加3个函数
(1).默认构造函数(无参,函数体为空)
(2).默认析构函数(无参,函数体为空)
(3).默认拷贝构造函数,对类中非静态成员属性简单值拷贝
如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造
2.构造/析构
(1)构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
(2)析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
3.浅拷贝/深拷贝
(1)一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
(2)当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。