先分享这个可不思议的结果,再来分析为什么会这样。
情况是:实例化Branch
类为bat1
,给bat1
的构造函数传递参数,将private
的成员变量m_a,m_b,m_c
分别赋值为1,2, 3。通过容器emplace_back
传参后,调用构造函数,将构造函数生成的对象存入到容器中。打印出容器的内容,此时容器branches_[0]中m_a,m_b,m_c的值为1, 2, 3。
接着调用赋值构造函数,生成对象bat2。在构造函数中定义m_a,m_b,m_c的值为4, 5, 6。再通过容器emplace_back
将对象bat2存入到容器中。最后打印出令人有点吃惊的结果:branches_[0] = {4,5,6}, branches_[1] = {4,5,6}
。在这里先提几个问题:
- 这个是浅拷贝还是深拷贝,bat1中m_a, m_b, m_c的值是否还是1, 2, 3
- 之前压入容器中的1, 2, 3中的值为什么会变成4, 5, 6?
- emplace_back和push_back有什么区别?
#include<iostream>
#include<vector>
using namespace std;
class Branch{
private:
int m_a;
int m_b;
int m_c;
public:
Branch(int a, int b, int c); // 构造函数
Branch(const Branch &a); // 拷贝构造函数
void Bond(); // Bond主要完成将m_a,m_b, m_c这三个变量压入到容器中
void show(); // 将容器中的值打印出来
};
vector<Branch> branches_;
// 容器branches_是Branch类,默认会调用Branch的构造函数参数为单位往容器中压入元素
Branch::Branch(int a, int b ,int c) : m_a(a), m_b(b), m_c(c){
cout <<"hi" << endl; // 通过构造函数列表进行初始化private变量,等同于m_a = a, m_b = b, m_c = c
}// 每次调用构造函数的时候都打印出一个“hi”
Branch::Branch(const Branch &a){
this->m_a = 4; // 定义赋值构造函数,并改变变量的值
this->m_b = 5;
this->m_c = 6;
cout << "copy_copy" << endl; // 一旦出现copy_copy就说明调用了构造函数
}
void Branch::Bond(){ // 调用构造函数将对象压入到容器中
branches_.emplace_back(m_a, m_b, m_c);
}
void Branch::show(){ // 将容器中的值打印出来,此处默认直接将容器中[0][1]的值都打印出来,也可以使用迭代器
cout << "m_a = " << m_a << endl; // 打印出实例化对象中变量m_a的值
for(int i = 0; i < 2; i++){
cout << "branches_[" << i << "]" << ".m_a = " << branches_[i].m_a << endl;
cout << "branches_[" << i << "]" << ".m_b = " << branches_[i].m_b << endl;
cout << "branches_[" << i << "]" << ".m_c = " << branches_[i].m_c << endl;
}
}
int main(){
Branch bat1(1, 2, 3); // 实例化对象,调用构造函数并初始化成员变量
bat1.Bond(); // 将bat1的三个成员变量压入到容器中
bat1.show(); // 打印容器中的内容
Branch bat2 (bat1); // 调用赋值构造函数,实例化为bat2
bat2.Bond();
bat2.show();
return 0;
}
什么是vector
vector是c++相比于c非常重要的一个改变,在c中定义一个数组必须制定数组的长度,在后续使用的过程中没办法随意缩减或者增加,而c++中vector可以不停地往这个容器中添加元素。
在android art源码中,很容易看到基于类的容器比如上述代码中:vector<Branch> branches_;
表示这个是类的容器,我们可以用二维数组的方式进行理解,branches_[],‘[]’表示的是有多个对象,每个组中分别是构造函数的参数的个数,在android这样大型的项目中,可能还会涉及到构造函数的重载和构造函数的参数初始化。比如:
上图展示的就是在art中关于branches_容器的定义,容器为类容器,给容器中压入参数是以构造函数为参数的类型重载匹配后,调用构造函数,在容器中压入的构造函数生成的对象,该对象中有类的所有成员变量,因为对象是由成员变量和成员函数组成的,而成员函数在内存中不占空间,但是成员变量是占据空间的
容器的定义也可以有以下几种:
void test01() {
vector<int>v1;//默认构造,无参构造
for (int i = 0; i < 10; i++) {
v1.push_back(i);
}
printVector(v1);
//通过区间方式进行构造
vector<int>v2(v1.begin(), v1.end());
printVector(v2);
//n个elem方式构造
vector<int>v3(10, 100);
printVector(v3);
//拷贝构造
vector<int >v4(v3);
printVector(v4);
}
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
100 100 100 100 100 100 100 100 100 100
100 100 100 100 100 100 100 100 100 100
容器的内容的获取与打印
容器中压入参数的方式有push_back和emplace_back两种。获取容器中内容的方式有四种,此处针对类的容器进行分析。
第一种:直接像二维数组一样直接获取容器中的元素,如:branches_[i].m_a
第二种:(*branches_.begin()).m_b
,branches_.begin()获得是的容器首元素的地址,解引用后得到branches_[0]。这种情况和二维数组是一样的,帮大家复习一下:
#include<iostream>
using namespace std;
int main(){
int a[][3] = {{1,2,3},{4,5,6},{7,8,9}};
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "*a: " << *a << endl;
cout << "(*a)[0]: " << (*a)[0] <<endl;
return 0;
}
a: 0x7ffef7160e60 // 直接用二维数组的返回的是首元素的地址
a[0]: 0x7ffef7160e60 // a[0]也是首元素的地址
*a: 0x7ffef7160e60 // 但是对a表示的地址进行解引用,得到的不是首元素,而是a[0],还是一个地址,只是表示的是第一个数组的地址
(*a)[0]: 1 // 对a的地址解引用得到的是一个数组的地址,对首地址进行解引用,获取第0个元素的内容就等于为a[0][0],就可以得到元素1
同样branches_.end()
可以获取容器中末尾元素的地址。
第三种:branches_.back().m_b
可以获得末尾元素的引用,branches_.front().m_b可以获得首元素的引用。
第四种: 利用迭代器获取容器中的元素,用法为:
for(vector<Branch>::iterator it = branches_.begin(); it != branches_.end(); it++){
cout << (*it).m_a << " " <<(*it).m_b << " " <<(*it).m_c << " " << "\n";
}
需要注意的是,如果在循环中打印it的值,指向的永远是一个地址,即迭代器本身的地址是不会变的。
#include<iostream>
#include<vector>
using namespace std;
class Branch{
private:
int m_a;
int m_b;
int m_c;
public:
Branch(int a, int b, int c);
void Bond();
void show();
};
vector<Branch> branches_;
Branch::Branch(int a, int b ,int c) : m_a(a), m_b(b), m_c(c){
}
void Branch::Bond(){
branches_.emplace_back(m_a, m_b, m_c);
cout << "branches_.size() = " << branches_.size() << endl;
cout << "branches_.back().m_b = " << branches_.back().m_b << endl;
cout << "(*branches_.begin()).m_b = " << (*branches_.begin()).m_b << endl;
cout << "branches_[0].m_c = " << branches_[0].m_c << endl;
}
void Branch::show(){
for(vector<Branch>::iterator it = branches_.begin(); it != branches_.end(); it++){
cout << (*it).m_a << " " <<(*it).m_b << " " <<(*it).m_c << " " << "\n";
}
}
int main(){
Branch bat1(1,2,3);
bat1.Bond();
bat1.show();
cout << "=============" << endl;
Branch bat2(4,5,6);
bat2.Bond();
bat2.show();
return 0;
}
branches_.size() = 1
branches_.back().m_b = 2
(*branches_.begin()).m_b = 2
branches_[0].m_c = 3
1 2 3
=============
branches_.size() = 2
branches_.back().m_b = 5
(*branches_.begin()).m_b = 2
branches_[0].m_c = 3
1 2 3
4 5 6
容器中压入的是参数还是参数的地址?
回到文章开头,为什么容器中的值123会变成456,会不会是因为bat1中的值发生了变化,容器存放的是不是变量的地址,然后发生的改变呢?那么我们来做个实验:
#include<iostream>
#include<vector>
using namespace std;
int main(){
int a = 1, b = 2, c = 3;
vector<int>D;
D.emplace_back(a);
D.emplace_back(b);
D.emplace_back(c);
for(vector<int>::iterator it = D.begin(); it != D.end(); it++){
cout << *it << " "<< endl;
}
cout << "======" << endl;
a = 100;
D.emplace_back(a);
for(vector<int>::iterator it = D.begin(); it != D.end(); it++){
cout << *it << " "<< endl;
}
return 0;
}
1
2
3
======
1
2
3
100
先定义了三个int的元素,然后存放到容器中,接着对变量a的值进行改变,从1变成100,这个时候再将a存入到容器中,分别打印之前容器中的值和之后容器中的值。可以发现:变量a在容器中的值1并没有发生改变,改变变量a推入容器后,只是在容器后继续存入了值为100的数。所以容器其实就是数组,你给数组中a[0]赋值,那么就是在一块新开辟的空间中存入这个数。
所以导致文章开头容器中的值发生改变的根本原因并不是因为bat1中成员变量的值发生了改变,也不是容器中存放的是成员变量的地址。接着我们来讨论一下是什么原因让我们怀疑bat1中成员变量的值可能发生改变?
浅拷贝与深拷贝
通俗的来讲,浅拷贝就是不定义拷贝构造函数进行的拷贝,拷贝出来的对象与之前的对象其实指向的是同一块内存空间,这个与fork的原理有些类似。在没有定义拷贝构造函数而进行拷贝的时候,编译器会自动调用默认拷贝构造函数进行浅拷贝。
#include <iostream>
#include <cstdlib>
using namespace std;
class Array{
public:
Array(int len);
~Array();
public:
int operator[](int i) const { return m_p[i]; } //获取元素(读取)
int &operator[](int i){ return m_p[i]; } //获取元素(写入)
int length() const { return m_len; }
private:
int m_len;
int *m_p;
};
Array::Array(int len): m_len(len){
m_p = (int*)calloc(len, sizeof(int) );
}
Array::~Array(){ free(m_p); }
//打印数组元素
void printArray(const Array &arr){
int len = arr.length();
for(int i=0; i<len; i++){
if(i == len-1){
cout<<arr[i]<<endl;
}else{
cout<<arr[i]<<", ";
}
}
}
int main(){
Array arr1(10);
for(int i=0; i<10; i++){
arr1[i] = i;
}
Array arr2 = arr1;
arr2[5] = 100;
arr2[3] = 29;
printArray(arr1);
printArray(arr2);
return 0;
}
0, 1, 2, 29, 4, 100, 6, 7, 8, 9
0, 1, 2, 29, 4, 100, 6, 7, 8, 9
可以发现在浅拷贝的时候,arr2的成员变量内容的修改也会导致arr1成员变量的修改。
但如果一个类拥有指针类型的成员变量或者需要在创建对象时对成员变量进行预处理,比如次数统计,时间等就需要对拷贝构造函数进行自定义,这个时候需要进行深拷贝。在深拷贝的之后,拷贝出的对象与原对象拥有不同的内存空间,二者之间彼此独立。
反观文章开头的例子,由于我们定义了拷贝构造函数并给成员变量进行了重新赋值,那么bat2的拷贝是一个深拷贝,也就是说bat1中的成员变量中的值并不会发生改变。如果在bat2.show()显示完之后再调用bat1.show(),打印出的m_a的值并没有发生变化,还是1。
vector内容发生改变的原因——容器扩容
在进行了多次尝试之后,如果给容器初始化的时候就给定一个足够大小的空间,该现象就不会出现。
由于一开始并没有给容器制定空间,默认初始大小为1,当拷贝构造函数bat2继续往容器中添加时,容器的内存不够了,需要*2的方式进行扩容。在扩容的过程中,vector需要把旧值拷贝到新的空间中,在这个过程中会调用到构造函数与拷贝构造函数,于是容器中的emplace.back就直接把拷贝构造函数塞进容器中了,但是注意,由于还是定义了拷贝构造函数并赋值了,此处是深拷贝,bat1中的成员变量的值不会改变,而是在vector扩容的过程中自动调用了拷贝构造函数将本来要塞进容器中的对象bat1变成了拷贝出的bat2。
vector——emplace.back与push.back
emplace.back不会调用拷贝构造函数,但是push.back会。假如我们往容器中添加对象的方式是push.back,那么即便容器中的大小已经初始化成足够大的大小,容器中存放的对象却是拷贝构造函数对象。
总结
这个例子是在学习Android art代码的时候自己敲出来的例子,虽然用法有点不太正规,但是很有趣,可以学习到类,成员变量,成员函数,容器,类容器,打印容器中的内容,构造函数,赋值构造函数,构造函数重载,浅拷贝深拷贝,容器扩容等知识。简单,但涵盖的面很多。