C++基础2(2):类与对象

4. 方法

4.1 构造函数

4.1.1. 语法

类名(参数){
    函数体
}

特点

  • 在对象被创建时自动执行
  • 构造函数的函数名与类名相同
  • 没有返回值类型、也没有返回值
  • 可以有多个构造函数

调用时机

  • 对象直接定义创建–构造函数不能被显式调用
  • new动态创建

默认构造函数

  • 类中没有显式的定义任何构造函数,编译器就会自动为该类型生成默认构造函数,默认构造函数没有参数。

构造函数的三个作用

  • 给创建的对象建立一个标识符
  • 为对象数据成员开辟内存空间
  • 完成对象数据成员的初始化

初始化列表

语法

类名(参数):成员变量(参数){
  函数体
}

作用

初始化非静态成员变量

说明

从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。

  • 必须使用初始化列表的情况
    1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面。
    2.引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面。
    3.没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
  • 初始化列表与构造函数内部成员变量赋值的区别
    成员变量初始化与成员变量赋值

能使用初始化列表的时候尽量使用初始化列表

  • 案例:student.cpp
#include <iostream>
using namespace std;

class Student{
private:
	string name;
	bool sex;
	int age;
public:
	Student():name(),sex(true),age(0){} // 定义默认构造函数
	Student(string n,bool s,int a):name(n),sex(s),age(a){}
	void Print(){
		cout << "name:" << name << endl;
		cout << "sex:" << (sex?"men":"women") << endl;
		cout << "age" << age << endl;
	}
};

int main(){
	Student Tom("Tom",true,21);
	Tom.Print();
	
	Student* Emma = new Student("Emma",false,22);
	Emma->Print();	
	Student Jack;	// 像这种没有加参数的对象定义,是调用的默认构造函数
					// 如果类里面定义其他的构造函数,但是没有定义默认构造函数,那么默认构造函数不能使用
	Jack.Print();
}

成员变量的初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

C++的函数可以增加默认参数。
写法:

  1. 默认参数必须写在函数声明中,不能写在函数实现的参数列表中中。
  2. 默认参数必须写在所有非默认参数的后面。
  3. 默认参数可以写任意多个。

使用:

  • 默认参数可以不用传递值,此时,使用默认值。
  • 默认参数可以传值,此时,使用实参值。
  • 案例:Bill.cpp
#include <iostream>
#include <vector>
using namespace std;
class Record{
private:
	string name;
	int count;
	float price;
	float off;
public: // 构造函数+初始化列表
	Record(string name,int count,float price):name(name),count(count),price(price),off(1){}
	// 构造函数重载
	Record(string name,int count,float price,float off):name(name),count(count),price(price),off(off){}
	float GetTotal(){
		return count*price*off;
	}
	float GetOff(){
		return count*price*(1-off);
	}
	void Print(){
		cout << name << '\t' << count << "\t¥" << price << "\t¥" << GetTotal() << "\t¥" << GetOff() << endl;
	}
};
class Bill{
private:
	vector<Record> records;
public:
	void Add(Record r){
		records.push_back(r);
	}
	void Print(){
		cout << "物品\t数量\t单价\t总价\t节省\n";
		cout << "---------------------------------------" << endl;
		float totalPrice = 0;
		float totalOff = 0;
		for(int i=0;i<records.size();++i){
			records[i].Print();
			totalPrice+=records[i].GetTotal();
			totalOff+=records[i].GetOff();
		}
		cout << "---------------------------------------" << endl;
		cout << "总价:¥" << totalPrice << "\t" << "总节省:¥" << totalOff << endl;
	}
};

int main(){
	Bill b;
	b.Add(Record("苹果",3,3.5)); // 匿名函数
	b.Add(Record("橘子",4,5.5));
	b.Add(Record("梨",2,1.5,0.7));
	b.Add(Record("香蕉",4,4.5,0.8));
	b.Print();
}

4.2 析构函数

语法:

~类名(){
    函数体
}
  • 析构函数的函数名与类名相同
  • 函数名前必须有一个~
  • 没有参数
  • 没有返回值类型、也没有返回值
  • 只能有一个析构函数
  • 析构函数的执行顺序与声明顺序完全相反,最先构造的最后析构

调用时机

  • 对象离开作用域
  • delete

默认析构函数

类中没有显式的定义析构函数,编译器就会自动为该类型生成默认析构函数

作用

释放对象所申请占有的资源
RAII(资源的取得就是初始化,Resource Acquisition Is Initialization)
C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。 – 百度百科

分析下面程序执行结果:

#include <iostream>

using namespace std;

class Test{
public:
    Test(){
        cout << "Test Construct" <<endl;
    }
    ~Test(){
        cout << "Test Deconstruct" << endl;
    }
};

int main(){
    // 局部对象
    cout << "Before {" << endl;
    {
        cout << "After {" << endl; 
        Test t;
        cout << "Before }" << endl;
    }
    cout << "After }" << endl;

    // 动态对象
    cout << "Before {" << endl;
    {
        cout << "After {" << endl; 
        Test* pt = new Test;
        delete pt;
        pt = NULL;
        cout << "Before }" << endl;
    }
    cout << "After }" << endl;
    return 0;
}

问题: new/delete与malloc()与free()的区别?

new 来定义一个对象的时候会自动的调用这个对象对应的构造函数,delete 会自动的调用这个对象的析构函数。而 malloc() 和 free() 不会。

4.3 引用(别名)

4.3.1 语法

声明:const 类型名& 对象名/类型名& 对象名
使用:与对象变量、基本类型变量一样

① 引用的用法:

int& b = a;  

②定义好变量,后面的&表示取地址符号;

cout << "&b:" << &b << endl;

③引用其实就是一个别名,a与b代表的是相同的对象

  • 案例:reference.cpp
#include <iostream>
using namespace std;

int main(){
	int a(10);
	int& b = a; // 定义引用
	cout << "&a:" << &a << "\t" << a << endl; // &取地址
	cout << "&b:" << &b << "\t" << b << endl;
	 a = 100;
	cout << "&a:" << &a << "\t" << a << endl;
	cout << "&b:" << &b << "\t" << b << endl;
	 b = 1000;
	cout << "&a:" << &a << "\t" << a << endl;
	cout << "&b:" << &b << "\t" << b << endl;


}

4.3.2 注意

  1. 定义的时候必须初始化,即给定变量;
int m;
int& b;
b = m;    //错误1;
  1. 引用需要用变量进行引用,常量不可以引用;
int m;
int& b = 10;   //错误2
  1. 一个变量可以有多个引用,但是一个引用只能对应一个变量,不能修改为其他变量的别名;
int& b = m;
int& b = n;  //错误3

4.3.3 何处使用引用

  1. 避免空指针,取代指针的用法
    (1.1)避免空指针,取代指针的用法一:传递参数的三种方式(Swap.cpp):

①值传递(不能修改地址的内容)

#include <iostream>
using namespace std;


void Swap(int m, int n){
	int t = m;
	m = n;
	n = t;

}
int main(){
	int n = 10;
	int m = 100;
	Swap(n,m);
	cout << "n:" << n << "\tm:" << m << endl;
}

②指针传递(可以修改地址的内容,但是解引用比较麻烦)

#include <iostream>
using namespace std;

void Swap(int *m, int *n){
	int t = *m;  //t表示变量,而非指针;
	*m = *n;
	*n = t;

}
int main(){
	int n = 10;
	int m = 100;
	Swap(n,m);
	cout << "n:" << n << "\tm:" << m << endl;
}

③引用传递(避免空指针,野指针)

#include <iostream>
using namespace std;

void Swap(int &m, int &n){
	int t = m;
	m = n;
	n = t;

}
int main(){
	int n = 10;
	int m = 100;
	Swap(n,m);
	cout << "n:" << n << "\tm:" << m << endl;
}

(1.2)取代指针的用法二:多个返回值

#include <iostream>
using namespace std;
int Divide(int n , int m, int& mod){
	int div = n/m;
	mod = n%m;
	return div;
}  
 int main(){
 int m(101);
 int n(10);
 int mod;
int div =  Divide(m,n,mod);
 cout << "div:" << div << "\tmod:" << mod << endl;
 
 }
  1. 函数参数列表
  2. 函数返回值
  3. 成员变量 – 对象初始化时,必须显示初始化的变量
  • 引用的三个特点
6. 引用必须初始化
7. 引用必须使用变量初始化 
8. 引用初始化后不能修改,只能是一个变量的别名,不能再修改成其他变量的别名 
  • 案例:reference_class.cpp
#include <iostream>
using namespace std;

class Simple{
private:
	int n;
public:
	Simple(int n):n(n){}
	void Print(){
		cout << "&n:" << &n << "\t" << n << endl;
	}
};
class Simple2{
private:
	int* n;
public:
	Simple2(int* n):n(n){}
	void Print(){
		cout << "n:" << n << "\t" << *n << endl; 
	}
};
class Simple3{
private:
	int& n;
public:
	Simple3(int& n):n(n){} // 引用成员变量必须在构造函数初始化列表中初始化
	void Print(){
		cout << "&n" << &n << "\t" << n << endl;
	}
};

int main(){
	Simple s(10);
	s.Print();
	int m = 10;
	cout << "&m:" << &m << "\t" << m << endl;
	// 指针
	Simple2 s2(&m);
	s2.Print();
	m = 13;
	s2.Print();
	// 引用
	Simple3 s3(m);
	s3.Print();
}

4.3.4 为何使用引用

  • 避免对象复制
  • 避免传递空指针
  • 使用方便
类型:
基本类型(bool、char、int、short、float)
复合类型(指针、数组、引用)
自定义类型(struct、union、class)

引用与指针的区别

  • 指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
  • 引用只能在定义时被初始化一次,之后不可变;指针可变;
  • 引用不能为空,指针可以为空;
  • 引用使用时无需解引用*,指针需要解引用;
  • sizeof 引用 得到的是所指向的变量/对象的大小,而sizeof 指针得到的是指针本身的大小;

引用通常用于两种情形:成员变量和函数的参数列表以及函数返回值。

  • 案例 reference_size.cpp
#include <iostream>
using namespace std;
class Types{
    int m;
    char c;
    bool b;
    float f;
    double d;
};
class Pointors{
    int* pm;
    char* pc;
    bool* pb;
    float* pf;
    double* pd;
};
class References{
    // 在结构体类型或者类类型中,在计算类型/对象时引用类型的成员变量大小与指针一样
    // 在计算单独引用类型成员变量时,引用成员大小与所引用的类型大小一致。
    int& fm;
    char& fc;
    bool& fb;
    float& ff;
    double& fd;
public:
    References(int& m,char& c,bool& b,float& f,double& d):fm(m),fc(c),fb(b),ff(f),fd(d){}
    void PrintSize(){
    cout << "sizeof(fm):" << sizeof(fm) << endl;
    cout << "sizeof(fc):" << sizeof(fc) << endl;
    cout << "sizeof(fb):" << sizeof(fb) << endl;
    cout << "sizeof(ff):" << sizeof(ff) << endl;
    cout << "sizeof(fd):" << sizeof(fd) << endl;
    }
};
int main(){
    int m;// 4
    char c; // 1
    bool b; // 1
    float f;// 4
    double d;//8

    // 各种类型引用的大小
    int& fm = m;
    char& fc = c;
    bool& fb = b;
    float& ff = f;
    double& fd = d;
    cout << "sizeof(fm):" << sizeof(fm) << endl;
    cout << "sizeof(fc):" << sizeof(fc) << endl;
    cout << "sizeof(fb):" << sizeof(fb) << endl;
    cout << "sizeof(ff):" << sizeof(ff) << endl;
    cout << "sizeof(fd):" << sizeof(fd) << endl;

    // 各种类型指针的大小
    int* pm = &m;
    char* pc = &c;
    bool* pb = &b;
    float* pf = &f;
    double* pd = &d;
    cout << "sizeof(pm):" << sizeof(pm) << endl;
    cout << "sizeof(pc):" << sizeof(pc) << endl;
    cout << "sizeof(pb):" << sizeof(pb) << endl;
    cout << "sizeof(pf):" << sizeof(pf) << endl;
    cout << "sizeof(pd):" << sizeof(pd) << endl;


    cout << "sizeof(Types):" << sizeof(Types) << endl;
    cout << "sizeof(Pointors):" << sizeof(Pointors) << endl;
    cout << "sizeof(References):" << sizeof(References) << endl;
    References r(m,c,b,f,d);
    r.PrintSize();
}
  • 结果显示
sizeof(fm):4
sizeof(fc):1
sizeof(fb):1
sizeof(ff):4
sizeof(fd):8
sizeof(pm):8
sizeof(pc):8
sizeof(pb):8
sizeof(pf):8
sizeof(pd):8

4.4 拷贝/复制构造函数

语法:

类名(类名& 形参){ 
    函数体
}

或者:

类名(const 类名& 形参){
    函数体
}

调用时机
手动调用

类名 对象名;  // 调用默认构造函数
类名 对象2 = 对象1;    // 调用复制构造函数
类名 对象3(对象1);     // 调用复制构造函数

自动调用

  • 一个对象作为函数参数,以值传递的方式传入函数体
  • 一个对象作为函数返回值,以值从函数返回
  • 一个对象用于给另外一个对象进行初始化(赋值初始化)

实践说明:

  • gcc/clang自动RVO/NRVO优化(匿名的返回值优化/有名字的返回值优化),不执行拷贝构造函数,会将两次拷贝构造省掉,相当于把局部对象的生存周期拉到了外面,延长了生存周期。可以在编译命令添加选项禁止 -fno-elide-constructors; (意思是不忽略拷贝构造函数)
  • VC在Debug环境下返回值执行拷贝构造函数,在Release(发布版)环境下实施RVO/NRVO优化。

-实例(调用时机测试)

#include <iostream>
using namespace std;
class Simple{
public:
	Simple(){
		cout << this << " Default Construction" << endl;
	}
	Simple(const Simple&){
		cout << this << " Copy Construction" << endl;
	}
	~Simple(){
		cout << this << " Destruction" << endl;
	}
};
void Func(Simple simple){}
Simple Func(){
	Simple s; // 使用默认构造函数定义带名字的对象时,对象名后最好不要加(),因为在一些旧的g++版本中会报错,报错的原因是将整行的语句Simple s();看作是一个函数的声明
	return s; // 匿名对象,这种写法很像在调用默认构造函数,实际只是定义一个对象而不是调用,并且()不能去掉
}
int main(){
	Simple s;
	/*
	Simple t = s;
	Simple k(s); // Simple k = s;
	*/
	// Func(s);
	Simple t = Func();
}

4.5 默认拷贝构造函数

  • 本质:内存拷贝
  • 作用:复制一个已经存在的对象

问题

  1. 一个类可以多个拷贝构造函数吗?
    答:可以有多个。
  2. 拷贝构造函数的参数可以不是引用吗?例如Test(Test t)。
    答:不可以,必须加引用Test(const Test& t)。
  3. 如何禁用默认拷贝构造函数?
    答:(1)在类定义,添加私有的拷贝构造函数的声明,但不实现
    private:
    Test(const Test&);
    (2)在C++11中
    Simple(const Simple&) = delete;

下列程序的执行结果

#include <iostream>
#include <string>

using namespace std;

class Test {
public:
  Test() {}
  Test(Test &t) { cout << "Test(Test &t)" << endl; }
  Test(Test const &t) { cout << "Test(Test const &t)" << endl; }
};

int main() {
  Test t1;
  Test t2 = t1;
  const Test t3;
  Test t4 = t3;
}

5. 赋值运算符重载函数

语法

类名& operater=(const 类名& 形参){
  // 赋值操作
  return *this;
}

调用时机

  • 赋值

默认赋值运算符重载函数

  • 内存拷贝

作用

  • 赋值
如何禁用默认赋值运算符重载函数?在类定义,添加私有的赋值运算符重载函数的声明,但不实现。

拷贝构造函数与赋值操作符的区别

  • 拷贝构造函数:当一个已经存在的对象来初始化一个未曾存在的对象
  • 赋值操作符:当两个对象都已经存在

案例

#include <iostream>
using namespace std;
class Simple{
public:
	Simple():p(NULL){
		cout << this << " Default Construction" << endl;
	}
	Simple(const Simple& s){
		p = new int(*(s.p)); // 深拷贝
		cout << this << " Copy Construction : " << p << endl;
	}
	Simple(int n){
		p = new int(n);
		cout << this << " Construction : " << p << endl;
	}
	// 赋值运算符重载
	Simple& operator=(const Simple& s){
		if(this == &s) return *this; // 1.判断是否是自身复制
		if(NULL != p){ // 2.必要时,删除之前申请的内存,防止内存泄漏
			delete p;
		}
		p = new int(*(s.p));
		cout << this << " operator= : " << p << endl;	
		return *this; // 3.返回当前对象
	}
	~Simple(){
		cout << this << " Destruction : " << p << endl;
	}
private:
	int* p;
};

int main(){
	Simple s(10);
	Simple k = s;
	Simple t;
	t = s; // 赋值
}

6. 深拷贝(Memberwise Copy)与浅拷贝(Bitwise Copy)

  • 浅拷贝:编译器默认生成的类实例间拷贝行为,对带有指针的类来说会引发 memory leak。
  • 深拷贝:用户定义的行为(实质是一种构造函数)。

问题
一个类中存在指针类型的成员变量。并且类构造和析构管理指针的申请与释放。

class Demo{
public:
    Demo(int n){
        p = new int(n);
    }
    ~Demo(){
        delete p;
        p = NULL;
    }
private:
    int* p;
};

调用拷贝构造函数会出现什么情况?

Demo a(10);
Demo b = a;
  • 案例 copy_copystruct2.cpp
#include <iostream>
using namespace std;

class Simple{
	int* p;
public:
	Simple(int n){
		cout << "Simple Construction" << endl;
		p = new int(n);
	}
	// 拷贝构造函数
	// 在函数对象按值传递时,会自动调用拷贝构造函数
	Simple(Simple& s){
		cout << "Simple Copy Construction" << endl;
		// p = s.p; // 动态分配的内存只拷贝内存地址就是浅拷贝,危害:二次/多次释放
		// 解决浅拷贝问题的方法:深拷贝
		// 重新申请内存,把数据完全拷贝到新的内存
		p = new int;
		*p = *(s.p);
	}
	~Simple(){
		cout << "Simple Destruction free:" << p << endl;
		delete p;
		p = NULL;
	}
	void Print(){
		cout << *p << endl;
	}
};

void Print(Simple simple){
	simple.Print();
}
int main(){
	Simple s(100);
	Print(s);
}

区别

  • 浅拷贝:只拷贝指针地址
  • 深拷贝:重现分配堆内存,拷贝指针指向内容

最佳实践
三大定律(Rule of three / the Law of The Big Three / The Big Three)
如果类中明确定义下列其中一个成员函数,那么必须连同其他二个成员函数编写至类内,即下列三个成员函数缺一不可:

  • 析构函数(destructor)
  • 复制构造函数(copy constructor)
  • 复制赋值运算符(copy assignment operator)

案例

#include <iostream>
#include <cstring>
using namespace std;

class String{
private:
	char* str;
public:
	String():str(NULL){
		str = new char[1];
		str[0] = '\0'; // 空串
	}
	String(const char* s){
		str = new char[strlen(s)+1];
		strcpy(str,s);
	}
	String(const String& s){
		str = new char[strlen(s.str)+1];
		strcpy(str,s.str);
	}
	String& operator=(String& s){
		if(&s == this) return *this; // 1. 判断是否是自身赋值
		delete [] str;	// 2. 删掉原来申请的内存
		str = new char[strlen(s.str)+1]; // 3. 重新分配内存
		strcpy(str,s.str);
		
		return *this; // 4. 返回对象引用
	}
	~String(){
		cout << "delete " << (void*)str << endl;
		if(NULL!=str) delete [] str;
		str = NULL;
	}
	void Print(){
		cout << str << endl;
	}
};

int main(){
	String str1;
	str1.Print();
	String str2("Hello");
	str2.Print();
	
	String str3(str2); // 拷贝构造函数
	str3.Print();

	str1 = str2; // 赋值运算符重载函数
	str2.Print();

	str2 = str2; // 自我赋值特例
	str2.Print();

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值