类和对象拷贝构造函数的调用时机
c++中拷贝构造函数调用时机通常有三种情况
1.使用一个已经创建完毕的对象来初始化一个新对象
2.值传递的方式给函数参数传值
3.以值方式返回局部对象
#include<iostream>
using namespace std;
class person
{
public:
person()
{
cout<<"person默认构造函数调用"<<endl;
}
person(int age)
{
m_age=age;
cout<<"person有参构造函数调用"<<endl;
}
person(const person &p)
{
m_age=p.m_age;
cout<<"person拷贝构造函数的调用"<<endl;
}
~person()
{
cout<<"person析构函数调用"<<endl;
}
int m_age;
};
我们在这里创建了一个person类,接下来,我会演示每一种调用的例子。
完毕的对象来初始化一个新对象
void test1()
{
person p1(20);
personp2(p1);
}
这里我们创建了两个person对象,我们利用p1的有参构造,给p1的m_age进行赋值操作,
接下来我们就可以使用一个已经创建完毕的对象p1来初始化一个新对象p2。我们打印p2的age值就可以得到和p1一样的age值。
值传递
void chuandi(person p)
{
}
void test2()
{
person p;
chuandi(p);
}
如果我们运行这个test2,我们会发现我们这里调用了一次拷贝构造函数和两次析构函数,我们这时候可以这么理解:我们先创建了一个对象p,然后我们调用chuandi函数的时候又产生了一个形参(我理解为虚拟对象),所以说chuandi(p)和void chuandi (person p)的p不是一个p,这个函数的作用就是把p的值传递给另一个对象。相当于隐式转换法(person p=p)。(我是这么理解的不对的地方请在评论区大家指出)。
这就是第二中调用情况,值传递。
值方式返回局部对象
person dowork()
{
person p3;
return p3;
}
void test3()
{
person p4=dowork();
}
在通常情况下我们运行这个test3函数我们会看到这里调用了无参构造函数,拷贝构造函数,两簇析构函数 ,这里的返回值不会返回p3,而是会拷贝一次p3,然后再把值传递给p4。按正常情况是这样的,但是我们在使用vs2022等编译器时,发现并没有出现拷贝函数的调用而且析构函数只调用了一次,实际上这个拷贝函数是有被调用的,我看了其他博主,他们提到这是由于编译器的ROV优化,至于这个具体的机制我没看懂,大家可以百度一下。所以我们还是按照原来的理解方式去理解。
当然我们也可以通过打印m_age的地址去验证这个思路,当然在vs2022等编译器里我们还是会发现出来了两个相同的地址,而按我们的理解是会出现两个不同的地址的。
深拷贝与浅拷贝
简单来说,浅拷贝就是进行简单的赋值操作,比如编译器默认提供的拷贝函数就是一种浅拷贝;深拷贝就是在栈区重新申请一块空间,进行拷贝操作。
浅拷贝
#include<iostream>
using namespace std;
class person
{
public:
int age;
int* high;
person()
{
cout << "person无参构造函数调用" << endl;
}
person(int a,int b)
{
age = a;
high = new int(b);
cout << "person有参构造函数调用" << endl;
}
~person()
{
if(high!=NULL)
{
delete high;
high=NULL;
}
cout << "析构函数调用" << endl;
}
};
void test1()
{
person p1(18,180);
person p2(p1)
}
这上面我们使用了person的默认拷贝构造函数,它的默认构造函数其实相当于写了
age=p1.age;
high=p.high;
我们这里开辟了一个栈区,这时候我们需要编写析构函数进行栈区释放,这是一个需要注意的点,如果我们去运行这段代码,我们会发现出错。接下来我们解释一下原因。
这里其实是进行了简单的值拷贝,我觉得其实浅拷贝就像值传递,而 深拷贝相当于地址传递,当然这种说法不是很准确。如果我们这里对p1,p2的high地址进行打印,我们会发现他们的地址是一样的,所以我们这里调用析构函数时会对这栈区进行两次释放,这显然就是报错的原因。析构函数调用遵循先进后出,这里先对p2的high进行释放,然后p1再调用了一次析构函数,导致多次对同一栈区进行内存释放,导致出错。
深拷贝
#include<iostream>
using namespace std;
class person
{
public:
int age;
int* high;
person()
{
cout << "person无参构造函数调用" << endl;
}
person(int a, int b)
{
age = a;
high = new int(b);
cout << "person有参构造函数调用" << endl;
}
person(const person& p)
{
age = p.age;
high = new int(*p.high);
}
~person()
{
if(high!=NULL)
{
delete high;
high=NULL;
}
cout << "析构函数调用" << endl;
}
};
void test1()
{
person p1(18, 180);
person p2(p1);
cout << p1.age << " " << p1.high << endl;
cout << p2.age << " " << p2.high << endl;
}
int main()
{
test1();
system("pause");
return 0;
}
我们知道默认的拷贝构造函数是浅拷贝,所以这里需要我们重新写一个深拷贝,在这里age直接进行值拷贝是没有问题的,但是这个指针直接进行拷贝就会出现我们刚才的问题,所以我们这里用new int *开辟了一块新的空间,这样p2的high地址就和p1的不同,我们在调用析构函数的时才不会出错。
初始化列表
在C++中有一种语法可以初始化属性,叫初始化列表。它的语法很简单:构造函数():+属性1(初始化的值),属性2()·······
接下来我们演示一些他的用法。
class son
{
public:
int m;
int l;
int g;
son(int a, int b, int c) :m(a), l(b), g(c)
{
}
};
void test1()
{
son p(10,20, 30);
}
我们这里完成了对对象son属性的初始化。
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
例如:class A{};
class B{ A a};
这里就是一个最简单的对象成员,A类对象a作为B类的成员。这里我们会产生疑惑,A与B的构造函数和析构函数的顺序是谁先谁后?我们用一段代码来寻找这个问题的答案。
#include<iostream>
using namespace std;
#include<string>
class phone
{
public:
phone(string s)
{
cout << "phone 构造函数的调用" << endl;
phoneid = s;
}
string phoneid;
~phone()
{
cout << "phone析构函数调用" << endl;
}
};
class person
{
public:
person(string a, string b) :name(a), m_phone(b)
{
cout << "person构造函数调用" << endl;
}
string name;
phone m_phone;
~person()
{
cout << "person析构函数调用" << endl;
}
};
void test1()
{
person p("李四","iphoneX");
cout << p.name << "的手机是" << p.m_phone.phoneid << endl;
}
int main()
{
test1();
system("pause");
return 0;
}
我们运行这段代码我们会得到:李四的手机是iphoneX以及一系列的函数调用。这里m_phone(b)用到了隐式转换法,相当于phone m_phone=b。我们会看到这里的phone构造函数先被调用,再调用了person构造函数,这说明类成员的构造函数会先于类对象调用,然后person的析构函数先被调用,再调用phone的析构函数。这里我把类成员理解为零件,类对象理解为一台完整的机器,在组装机器时,要先组装完零件,再组装机器,拆解机器时先把整体拆出来,再对零件进行拆解。
总结:类成员构造函数调用先于类对象,类对象析构函数调用先于类成员。
静态成员
静态成员是指在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为:
1.静态成员变量
特征:所有对象共享同一份数据
在编译阶段分配内存
类内声明类外初始化
2.静态成员函数
特征:所有对象共享一个函数
静态成员函数只能访问静态成员变量
接下来简单演示一下他们的用法:
#include<iostream>
using namespace std;
#include<string>
class person
{
public:
static int a;
};
int person::a = 100;
void test1()
{
person p1;
cout << p1.a << endl;
person p2;
p1.a = 200;
cout << p2.a << endl;
}
因为静态成员不属于任何一个对象,所以可以有两种访问方式,一种是通过访问对象,一种是通过访问类名。这里可以用person::a来访问。我们运行一下这段代码,可以发现p1的a值为100,p2的a值为200,这说明a的值会被覆盖。
#include<iostream>
using namespace std;
#include<string>
class person
{
public:
static void fanc()
{
a = 100;
cout << "static void fanc调用" << endl;
}
static int a;
};
int person::a = 0;
void test1()
{
person p1;
p1.fanc();
}
int main()
{
test1();
cout << person::a << endl;
system("pause");
return 0;
}
静态成员函数和静态成员变量非常相似,只需要注意静态成员函数只能调用静态成员变量就行了。静态成员函数和静态成员变量都需要注意权限问题。
C++对象模型和this指针
在C++中,类内成员和类内函数是分开储存的,只有非静态成员变量才属于类的对象上。 这里我们很好去验证,我们只需要将各种对象的占用内存情况进行分析就可以得知
class person{};
test1(){person p1;
seziof(p1) ;}
class person{
int a;
};
test1(){person p1;
seziof(p1) ;}
class person{
int a;
void fanc();
};
test1(){person p1;
seziof(p1) ;}
我们只需要计算这几个字节长度就可以发现分别是1,4,4;这里第二个注意一下是4,而不是5,然后我们发现确实只有非静态成员变量属于该对象 。
在c++中每一个非静态成员函数只会诞生一份函数实例,多个同类型对象会共用一块代码,c++是怎么知道哪个对象在调用自己的呢?
C++通过提供特殊的对象指针this指针,this指针指向被调用的成员函数所属的对象。
注意:this指针隐含在每一个非静态成员函数中,不需要声明直接引用就可以。
用途:当形参和变量名 同名时,可以用this指针来区分;在类的非静态成员函数中返回对象本身,可使用return *this。
#include<iostream>
using namespace std;
class person
{
public:
person(int age)
{
this->age = age;
}
int age;
};
void test()
{
person p1(10);
}
这里我们如果直接写age=age我们会得到一个乱码,用this指针很好地区分了这两个变量。
#include<iostream>
using namespace std;
class person
{
public:
person(int age)
{
this->age = age;
}
person& add(person& p)
{
this->age += p.age;
return *this;
}
int age;
};
void test()
{
person p1(10);
person p2(10);
p2.add(p1).add(p1).add(p1);
cout << p1.age << endl;
cout << p2.age << endl;
}
这里add这个函数内this是指向p2的指针,而*this指向的是p2的本体,所以我们这里返回了p2这个对象,如果我们这里把person& add(person& p)第一个&去掉(解引用),我们就会发现p2的age值变成了20,这是因为我们这里返回了一个值,这里相当于用了拷贝构造,创造了p2‘,p2’‘,而不是不继续,只是指向了另一个内存。
空指针访问成员函数
c++中空指针也是可以调用成员函数的,但是要注意this指针的使用,如果用到this的指针需要用判断来保证代码的健壮性。
#include<iostream>
using namespace std;
class person
{
public:
void showname()
{
cout << "this is person class" << endl;
}
int age;
void showage()
{
cout << age << endl;
}
};
void test1()
{
person* p = NULL;
p->showname();
p->showage();
}
这里我们的代码是无法运行的,因为这里的空指针使用了this,在showage中其实age是this->age,这里的this是空指针,所以编译器不知道调用的是哪个age。为了使我们的代码更健壮,showage可以加
if(this==NULL)return;
这样就不会报错了。
const修饰成员函数
常函数:
成员函数后加const就叫常函数;常函数内不可以修改成员属性;成员属性声明时加mutable后可以在常函数中修改 。
常对象:
声明对象前加const;常对象只能调用常函数。
#include<iostream>
using namespace std;
class person
{
public:
void showperson()const
{
this->b = 100;
}
mutable int b;
};
void test1()
{
person p1;
}
在这段代码中showperson是不能修改没有mutable前缀的变量的,这是因为this本质是一个指针常量,如果在成员函数后面加const,就相当于把this的指向也锁定,无法指向其他,所以没办法改变。
class person
{
public:
void showperson()const
{
this->b = 100;
}
mutable int b;
};
void test2()
{
const person p2;
p2.b = 100;
p2.showperson();
}
在常对象中,只能引用常函数,常对象中mutable变量还是可以被修改的。
友元
在c++中对象的私有属性可以借助友元来访问
友元的关键字为friend
实现方式:1.全局函数做友元
2.类做友元
3.成员函数做友元
#include<iostream>
#include<string>
using namespace std;
class building;
class gg
{
public:
gg()
{
h = new building;
}
building* h;
void vist();
};
class hh
{
public:
building* g;
hh()
{
g = new building;
}
void vist();
};
void goodfriend(building* a);
class building
{
friend void gg::vist(); // gg 中的 vist 函数做友元
friend class hh; // hh 类做友元
friend void goodfriend(building* a); // goodfriend 函数做友元
public:
building()
{
sittingroom = "客厅";
bedroom = "卧室";
}
string sittingroom;
private:
string bedroom;
};
void gg::vist()
{
cout << h->bedroom;
}
void hh::vist()
{
cout << g->bedroom << endl;
}
void goodfriend(building* a)
{
cout << "好朋友正在访问:" << a->sittingroom << endl;
cout << "好朋友正在访问:" << a->bedroom << endl;
}
这段内容比较简单,上面是运用的例子,这周的笔记就到这里。