封装:
属性和行为作为一个整体,来表现各种事物。
将属性和行为加以权限控制(private,public,protected)
一些术语:
属性(成员属性,成员变量),行为(成员函数,成员方法);统称为成员。
实例化(通过一个类,创建一个对象的过程)
注意我们可以创建一个指向类的指针或则引用,但这不属于实例化,同时我们也可以通过new函数来实例化一个堆区的对象
访问权限:
保护权限,私有权限可以被友元访问
控制私有成员的读写权限:
(1)读和写:
(2)只读:
(3)加一个判断:
(4)只写:
构造和析构函数 :
只会调用一次
构造函数在对象创建处会被调用,析构函数在对象所在作用域结束后会被调用
构造函数的分类方式:
这里值得注意的是当构造函数的调用运算符(也就是括号)内含有默认参数时是有参构造还是无参构造还要看实例化对象的时候是否传参。
1 class Person 2 { 3 public: 4 Person(int B=10):m_b(B) 5 { 6 } 7 int m_b; 8 }; 9 10 int main() 11 { 12 Person p(20);//此时上面的构造函数是有参构造,输出值是20 13 cout << p.m_b << endl; 14 system("pause"); 15 return 0; 16 }
1 class Person 2 { 3 public: 4 Person(int B=10):m_b(B) 5 { 6 } 7 int m_b; 8 }; 9 10 int main() 11 { 12 Person p;//此时上面的构造函数是无参构造,输出结果是10 13 cout << p.m_b << endl; 14 system("pause"); 15 return 0; 16 }
有参构造:
就是构造函数的括号中含有参数,有参构造也可以在函数体内对参数进行一些操作。最重要的目的是给成员属性赋初值。
拷贝构造:
只有真正的在拷贝构造的函数体中显式赋值拷贝的对象的成员才会拷贝给被拷贝对象
但是当自己不写拷贝构造函数时,系统会自动将拷贝的对象的成员属性传递给被拷贝对象的成员属性。
1 //Point(const Person& p) 2 //{ 3 // m_Age = p.m_Age; 4 // cout << "Person的拷贝构造函数调用" << endl; 5 //} 6 7 8 void test01() 9 { 10 Point p; 11 p.m_Age = 18; 12 Point p2(p); 13 cout << "p2的年龄为:" << p2.m_Age << endl; 14 } 15 int main() 16 { 17 test01(); 18 }
注意,拷贝构造函数中被拷贝者先被析构掉(压栈和弹栈的原理)
//该例子中没有用到默认构造函数 class Person { public: Person(int age):age_(age) { cout << "Person有参构造函数调用" << "地址为:" << this << endl; } Person(const Person& p) { cout << "Person的拷贝构造函数调用" <<"地址为:" <<this << endl; } ~Person() { cout << "Person的析构函数调用" << "地址为:" << this << endl; } int age_; }; void test01() { Person p1(19); Person p2(p1); } int main() { test01(); system("pause"); return 0; }
运行结果为:
创建对象的方法:
括号法创建对象:
显示法:
等号右边是一个匿名对象,然后取名为p2,p3后p2,p3发生有参构造和拷贝构造
匿名对象:
打印结果说明函数没有执行完毕,匿名函数就被释放掉了
隐式转换法:
不能用隐式构造赋予一个字符串字面值,也不能给多个成员赋值
拷贝构造何时会被调用:
函数以值传递的方式进行参数传递的时候,如果形参是对象的话,就调用了拷贝构造,当实参给形参初始化的时候相当于隐式转换法调用了拷贝构造函数
构造函数调用规则:
构造函数按参数分类可以分为有参构造和无参构造,按类型分类可以分为普通构造和拷贝构造。
默认情况下,C++编译至少给一个类添加3个函数
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值传递
构造函数调用规则如下:
如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供拷贝构造,这里注意初始值列表也是有参构造
如果用户定义拷贝构造函故,C++不会再提供其他构造函数
(编译器不会提供意味着必须要自己写,不写就用不了。编译器提供意味着自己不写也可以用)
析构函数的作用:
1 class Person 2 { 3 public: 4 Person() 5 { 6 cout << "Person的默认构造函数调用" << endl; 7 } 8 Person(int age,int height) 9 { 10 cout << "Person的有参构造函数调用" << endl; 11 m_Age = age; 12 m_Height = new int(height); 13 } 14 ~Person() 15 { 16 //析构代码,将堆区开辟数据做释放操作 17 if (m_Height != NULL) 18 { 19 delete m_Height; 20 m_Height = NULL; 21 cout << "Person的析构函数调用" << endl; 22 } 23 } 24 int m_Age; 25 int* m_Height; 26 };
浅拷贝和深拷贝(解决浅拷贝的问题必须自己定义拷贝构造和重载拷贝赋值运算符,自己新开辟一块堆区空间):
如果利用编译器提供的拷贝构造函数,就会做浅拷贝操作。
浅拷贝出现问题在于:对象中成员属性具有指针类型,并且该指针指向一块堆区的内存,这样在做拷贝构造时,当调用两个析构函数的时候就会造成堆区的内存重复释放。
也就是说P2会把P1所有内容原封不动的拷贝过来,当P2析构时释放掉m_Height所指向的堆区内存时后P1再来释放这块内存就会非法操作。
1 class Person 2 { 3 public: 4 Person() 5 { 6 cout << "Person的默认构造函数调用" << endl; 7 } 8 Person(int age,int height) 9 { 10 cout << "Person的有参构造函数调用" << endl; 11 m_Age = age; 12 m_Height = new int(height); 13 } 19 ~Person() 20 { 21 //析构代码,将堆区开辟数据做释放操作 22 if (m_Height != NULL) 23 { 24 delete m_Height; 25 m_Height = NULL; 26 cout << "Person的析构函数调用" << endl; 27 } 28 } 29 int m_Age; 30 int* m_Height; 31 }; 32 void test01() 33 { 34 Person p(18,168); 35 Person p2(p); 36 cout << "p的年龄为:" << p.m_Age << "身高为:" << *p.m_Height << endl; 37 cout << "p2的年龄为:" << p2.m_Age << "身高为:" << *p2.m_Height << endl; 38 } 39 int main() 40 { 41 test01(); 42 }
当我们没有定义拷贝构造函数的时候,并且类的成员属性有一个指针,该指针指向一个堆区空间,且释放这个堆区空间需要在析构函数中实现。此时我们利用拷贝构造函数构造p2的时候,系统就会将p1的堆区内存赋值给p2,在析构的时候p2首先释放了这块内存,p1接着又释放了一遍,这就造成了重复释放内存的问题。
解决方法是进行深拷贝,重新申请一块堆区内存,让P2中的指针指向这块堆区内存。
1 class Person 2 { 3 public: 4 Person() 5 { 6 cout << "Person的默认构造函数调用" << endl; 7 } 8 Person(int age,int height) 9 { 10 cout << "Person的有参构造函数调用" << endl; 11 m_Age = age; 12 m_Height = new int(height); 13 } 14 Person(const Person& p) 15 { 16 cout << "Person的拷贝构造函数调用" << endl; 17 m_Age = p.m_Age;20 //深拷贝操作 21 m_Height = new int(*p.m_Height); 22 } 23 ~Person() 24 { 25 //析构代码,将堆区开辟数据做释放操作 26 if (m_Height != NULL) 27 { 28 delete m_Height; 29 m_Height = NULL; 30 cout << "Person的析构函数调用" << endl; 31 } 32 } 33 int m_Age; 34 int* m_Height; 35 }; 36 void test01() 37 { 38 Person p(18,168); 39 Person p2(p); 40 cout << "p的年龄为:" << p.m_Age << "身高为:" << *p.m_Height << endl; 41 cout << "p2的年龄为:" << p2.m_Age << "身高为:" << *p2.m_Height << endl; 42 } 43 int main() 44 { 45 test01(); 46 }
解决方法就是自己定义一个拷贝构造函数,在拷贝构造函数中重新开辟一个堆区内存给指针,堆区内存里的内容是p1指向的堆区内容。
以上是拷贝构造函数(也就是初始化一个对象)时出现的深拷贝和浅拷贝问题。接下来看一下赋值过程中(拷贝赋值运算时,也就是两个已经存在的对象互相赋值)出现的深拷贝和浅拷贝问题。注意这个拷贝赋值运算可以是合成的拷贝赋值运算,也就是说它和其他运算符不同当运算的对象是我们自己定义的类型时得重载该运算符,而它可以由编译器定义,也可以自己定义。
1 Person p1(18); 2 Person p2(20); 3 p2 = p1;
当我们直接书写上面代码时编译器并不会报错,因为它是一个合成拷贝赋值运算。
1 class Person { 2 public: 3 Person(int age) { 4 p_age_ = new int(age); 5 6 } 7 ~Person() { 8 if (p_age_ != nullptr) { 9 delete p_age_; 10 p_age_ = nullptr; 11 } 12 } 13 //引用的原因是,一个对象被赋值之后还应该是它自己 14 Person& operator=(Person& p) { 15 if (p_age_ != nullptr) { 16 delete p_age_; 17 p_age_ = nullptr; 18 }//注意这个判断语句必须有,它体现了赋值的特点,先把原来的值擦除,在被赋予新的值 19 p_age_ = new int(*p.p_age_); 20 return *this; 21 } 22 int *p_age_; 23 24 }; 25 26 void test01() { 27 Person p1(18); 28 Person p2(20); 29 p2 = p1; 30 }
深拷贝连续的堆区内存:
1 using std::cout; 2 using std::endl; 3 template<class T> 4 class MyArray 5 { 6 public: 7 MyArray(int capacity) 8 { 9 Capacity_ = capacity; 10 size_ = 0; 11 pAdress = new T[Capacity_]; 12 } 13 MyArray(const MyArray& arr) 14 { 15 Capacity_ = arr.Capacity_; 16 size_ = arr.size_; 17 pAdress = new T[arr.Capacity_]; 18 for (size_t i = 0; i < arr.Capacity_; ++i) 19 { 20 pAdress[i] = arr.pAdress[i]; 21 } 22 } 23 MyArray& operator=(const MyArray& arr) 24 { 25 if (pAdress) 26 { 27 delete[]pAdress; 28 pAdress = nullptr; 29 Capacity_ = 0; 30 size_ = 0; 31 } 32 Capacity_ = arr.Capacity_; 33 size_ = arr.size_; 34 pAdress = new T[arr.Capacity_]; 35 for (size_t i = 0; i < arr.Capacity_; ++i) 36 { 37 pAdress[i] = arr.pAdress[i]; 38 } 39 return *this; 40 41 } 42 ~MyArray() 43 { 44 cout << "MyAarray的析构函数调用" << endl; 45 if (pAdress) 46 { 47 delete[]pAdress; 48 pAdress = nullptr; 49 } 50 } 51 private: 52 T* pAdress; 53 int Capacity_; 54 int size_; 55 };
其他类对象作为本类成员:
当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反。
静态成员:(存储在全局区,也叫类变量)
- 类内声明,类外初始化操作
1 class Person 2 { 3 public: 4 static int m_A; 5 }; 6 int Person::m_A=100;
- 所有对象都共享同一份数据
1 class Person 2 { 3 public: 4 static int m_A; 5 }; 6 int Person::m_A=100; 7 void test01() 8 { 9 Person p; 10 cout << p.m_A << endl; 11 Person p1; 12 p1.m_A = 200; 13 cout << p.m_A << endl; 14 } 15 int main() 16 { 17 test01(); 18 system("pause"); 19 return 0; 20 }
- 编译阶段就分配内存
- 静态成员变量不属于某个对象上,所有对象都共享同一份数据,因此静态成员变量有两种访问方式
通过对象进行访问
1 void test02() 2 { 3 Person p; 4 cout << p.m_A << endl; 5 }
通过类名进行访问
1 void test02() 2 { 3 cout << Person::m_A << endl; 4 }
静态成员函数:
静态成员函数没有this指针,因为多个实例化对象共用一个静态成员函数。不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态
静态函数是属于所有对象的,因此不能访问非静态成员变量,因为静态成员函数不知道这个非静态成员变量到底是属于哪个对象的
空对象占用的空间:
成员变量和成员函数分来存储,只有非静态成员变量才属于类的对象上的:
实例化类的对象,打印对象的大小,结果是4,也就是整型成员变量的大小,同样也注意非静态成员函数也只有一份。
this指针
非静态成员函数只有一份,怎么区分哪个对象调用它呢;哪个对象调用这个成员函数这个函数的this指针就指向谁,就代表这个对象在调用它。
this指针是隐含在每一个非静态成员函数内的一个指针(隐式参数),它不需要定义,直接使用即可。
this指针的用途
当形参和成员变量同名时,可用this指针来区分
如果是p这个对象调用这个成员函数,则this->age相当于(*this).age,*this就是p(因为this指向了p)。其实在非静态成员函数的内部访问成员属性的时候,成员属性前面默认的加this->,如果没加的话。
在类的非静态成员函数中返回对象本身,可使用return *this。
注意:
p这个对象在调用函数showPerson,this指针指向了p,这个时候在成员函数内部让this指向了其他值,这对于指针常量显然是不允许的。
1 class Person 2 { 3 public: 4 Person(int age) 5 { 6 m_age = age; 7 } 8 Person& PersonAddage(Person &p) 9 { 10 this->m_age += p.m_age; 11 return *this; 12 } 13 int m_age; 14 }; 15 void test01() 16 { 17 Person p1(10); 18 Person p2(10); 19 p2.PersonAddage(p1).PersonAddage(p1).PersonAddage(p1); 20 cout << "p2的年龄为:" << p2.m_age << endl; 21 } 22 23 int main() 24 { 25 test01(); 26 system("pause"); 27 return 0; 28 }
标黄处返回值类型如果是指针:
第一次return*this相当于返回了一个指向p2的引用,这意味着返回的还是p2自己,第二次的返回的时候又产生了一个新的引用去将第一个引用指向的对象(也就是p2)拷贝下来,这相当于还是p2本身,以此类推,因此无论多少次都是对p2进行操作。
标黄处返回值类型如果是指针:
这样就会出错因为返回值类型是对象的指针而*this是一个对象
标黄处返回值类型如果是对象:
在第一个点处p2.PersonAddage(p1),只有这次才是真正的调用p2的成员函数,接下来第一次返回时return处创建一个临时对象用来接收p2,这时发生了拷贝构造:Person temp=p2;这意味着就不再是p2了。下面的语句打印p2结果是20,也就是第一个点处调用p2的成员函数导致的。空指针访问成员函数:
1 class Person 2 { 3 public: 4 void Classname() 5 { 6 7 } 8 void Personage() 9 { 10 cout << "age" << m_age << endl;//此处相当于this->m_age,如果一个类指针的值为空,那么这个指针不能访问成员变量。 11 } 12 int m_age; 13 }; 14 void test01() 15 { 16 Person* p = NULL; 17 p->Personage(); 18 }
上面代码会报错,这个时候就应该和正常的指针一样,使用之前应该检查一下它是否为空。
常函数和常对象:
常函数:
成员函数后加const我们称为这个函数为常函数,此时this指针变成了指向常对象的指针常量,意味着指针的指向和指针指向的值不可以修改,也就是不可以通过this指针对对象的值修改,而常函数内“不可以修改成员属性(只读)”这句话应该这样理解:
1 int fun()const 2 { 3 m_A = 10; 4 }
m_A相当于this->m_A这就意味这函数体内部的成员属性都是通过this指针来进行访问的,修改也是通过this指针进行修改的,如果一个成员函数是常函数,则在其内部修改成员属性相当于通过const *int const来修改this所指向的对象的对象的成员属性,这显然是不合理的。
成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
声明对象前加const称该对象为常对象,常对象是在构造函数的时候被初始化的,一旦初始化完成,其值就不可以更改了。
因此如果一个类想要实例化成常对象就必须在类内初始化非mutable的成员变量(初始化成员变量也就是初始化对象),
普通对象可以调用常函数,常对象只能调用常函数,因为一旦常对象可以调用非常函数的话这就意味着非常函数可以修改常对象的成员属性,这显然是不合理的。虽然常对象的成员变量不可以修改,但常对象的成员变量前加了mutable关键字,该成员变量就可以通过常对象修改。
在构造函数中定义一个其他类的堆区对象
注意定义该堆区对象时,一样会调用该对象的构造函数。
1 class Building; 2 class GoodGay { 3 4 public: 5 GoodGay(); 6 void visit(); 7 Building* building_; 8 }; 9 10 class Building { 11 friend class GoodGay; 12 public: 13 Building(); 14 std::string sitttingroom_; 15 private: 16 std::string bedroom_; 17 }; 18 GoodGay::GoodGay() { 19 cout << "GoodGay类的构造函数" << endl; 20 building_ = new Building; 21 }