C++学习第五篇(类和对象)

1,struct和class区别:struct默认访问权限是public,而class默认访问权限是private(包括成员变量和成员函数),Java中默认是default权限,Java有四种权限,default是包内可以访问,其他包不行;

struct P1{
	int i;  //默认为public
}
class P2{
	int i;  //默认为private
}

2,在头文件中加入#pragma once可以防止重复包含,类的结构一般都是声明在头文件中(包括成员变量和函数的声明),其函数实现在cpp文件中:

#include a.h
#include b.h
//如果a.h和b.h中都存在x.h,那么x.h就会包含两次,拖慢编译速度
//解决方式一:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
...//(头文件内容)
#endif
//当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。但是第二次头文件仍然会被读入,哪怕里面内容会被忽略,还是会拖慢速度

// #pragma once是windows平台的宏,可以防止重复包含与重打开,直接放文件第一行,一步到位,直接解决

3,构造函数和析构函数:分别用于创建对象时执行初始化操作和对象销毁前执行清理操作,涉及安全问题,如果没有显式给出,编译器会自动加上,但都是空实现;
(1)C++默认提供三个函数:默认无参构造函数、析构函数、默认拷贝构造函数(将所有值拷贝,哪怕将属性声明为私有的也可以拷贝);
(2)如果写了有参构造函数,编译器不再提供默认无参构造函数;
(3)如果写了拷贝构造函数,编译器不再提供其他任何构造函数;
– 构造函数在创建对象时自动调用,可有参数用于初始化,但没有返回值;
– 析构函数语法: ~类名() ,同样没有返回值,也没有参数,在清理对象时自动调用,无需手动调用;

class Person{
private:
	int a;
public:
	Person(){    
	}
	Person(int a){
		this.a = a;
	}
	Person(const Person &p){  //拷贝构造函数,是Java中的浅拷贝
		this.a = p.a;
	}
	...
}
int main(){
	//1,括号法调用构造函数
	Person p0; //此处已经为对象p分配内存(在栈区),调用了默认的无参构造函数
	Person p1(10); //调用有参构造函数
	Person p2(p1); //调用拷贝构造函数

	//2,显式法
	Person p00;
	Person p11 = Person(10);
	Person p22 = Person(p11); //拷贝构造

	//3,隐式转换法
	Person p000 = 10; //等价于Person p000 = Person(10);
	Person p111 = p000; //等价于 Person p111 = Person(p000); 会调用拷贝构造函数,因此两个对象实际不在同一片空间,是浅拷贝
}

注意:
(1)调用无参构造方法时不要加括号:Person p();编译器会认为这是一个函数的声明(返回值类型是Person,函数名是p,该函数无参);
(2)Person(10)其意义是创建一个对象,初始化其成员变量a = 10;但这个对象没有名字(匿名对象),在这句执行之后会被马上回收掉;
(3)不要用拷贝构造函数初始化匿名对象:Person (p22); 会报错,这句等价于 Person p22;即新建对象p22,但p22已经被创建,会提示重定义;
(4)拷贝构造相当于Java中的浅拷贝,虽然指向不同的空间,但只是简单类型的值拷贝,指针类型的变量会直接复制地址,等于两个对象中的指针变量,指向的是同一个地址。如何深拷贝?可以在拷贝构造函数中这样写:

class Dog{
	...
public:
	int *a;
	Dog(const Dog &d){
		a = d.a; //浅拷贝写法
		a = new int(*d.a); //深拷贝写法
	}
	~Dog(){              //标准的带指针的析构函数写法
		if(a != NULL){
			delete a;
			a = NULL;  
		}
	}
	...
}

4,拷贝构造函数的调用时机
(1)使用已存在的对象创建新对象;
(2)以值传递的方式给函数传参数;
(3)值方式返回局部对象。
疑问:

class Dog{
public:
	Dog(){}
	Dog(const Dog &d){
		cout<<"拷贝函数被调用"<<endl;
	}
}
Dog test(){
	Dog d;
	return d;
}
int main(){
	test(); //此行打印一次
	test(); //此行打印一次
	cout<<"----------------"<<endl;
	Dog dog = test(); //此行打印一次
	return 1;
}

执行结束,线下面那行为什么不是打印两次?(函数调用打印一次,赋值在C++里表面拷贝构造又一次)线上面调用两次打印两次,表面没有缓存,每次调用函数都会调用拷贝构造函数,因此得出结论:针对此类语句,如果有匿名对象马上被赋给了其他对象,编译器有特殊优化(命名优化(Named Return Value Optimization, NRVO) ),只调用了一次拷贝构造函数。
在这里插入图片描述
上面我们说过,不要对匿名对象进行拷贝构造,会重定义,但此处test();不就是给以拷贝构造方式初始化匿名对象吗?这是因为两个对象不在同一个作用域(前者在test()函数中,后者在main()函数中),而且上一个对象也随着函数的结束而销毁,因此没问题。(或者理解为加了const 修饰 const Dog& 绑定 const Dog,没有问题)。

5,静态成员无论变量还是函数都被所有对象共享;静态成员变量在编译时期就已经分配空间,静态成员函数只能访问静态成员变量,不能访问普通成员变量,静态成员变量使用前要必须要在类外初始化:

class Person{
public:
   static int ban;  //如果在这里初始化 static int ban = 12;也会报错: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”

}
int Person::ban = 12;   //类外初始化,如果没有这步会报错:无法解析的命令。这个报错通常在链接阶段发生,认为ban变量已经分配内存但还没有初始化。

int main(){
	cout<< Person::ban<<endl;
}

一些额外知识:

Person0{
}
Person1{
public:
   int m_A;
}
Person2{
public:
	int m_A;
	static m_B;
	void func1(){};
	static func2(){};
}


int main(){
	Person0 p0;
	cout<<sizeof(p)<<endl;  //结果是1,类型是空的,对象也是空的,但是为了区分不同的空对象,还是给这些空对象分配一个字节的空间
	Person1 p1;
	cout<<sizeof(p1)<<endl;   //结果是4,非空的类型,里面只有一个int型变量,那就按4分配内存
	Person2 p2;
	cout<<sizeof(p2)<<endl; //结果是4,函数不论是不是静态的,都是共享一份,与成员变量是分开存储的,注意非静态成员变量也是仅有一份,在调用时,有方式(this指针)用于区分是哪个对象在调用它
}

6,this指针,本质是类型* const this如Person* const this;上面是this指针的用途之一:用于指向调用成员函数的对象,还有一个用途:

Person{
public:
	int m_A;
	Person& func(){
		return *this; //用于返回调用此成员函数的对象本体,如果返回值被声明为Person而不是Person&,那么返回的是一个克隆体(拷贝构造函数调用时机之一)
	}
}

注意:
C++中空指针允许访问不属于对象的内容(非静态成员函数、静态成员函数与静态变量):但成员函数中如果出现非静态成员变量,会报错:

Person{
public:
	int m_A;
	static int ban;
	void func0(){
		cout<<"abcdefg"<<endl;
	}
	void func1(){
		cout<<m_A<<endl;
	}
	static void func2(){
		cout<<ban<<endl;
	}
}
int Person::ban = 12;

int main(){
	Person* p = NULL;
	p->func0();//abcdefg
	p->func1();//报错,访问权限冲突,因为里面涉及非静态成员变量:cout<<m_A<<endl;自动会加上this:cout<< this->m_A <<endl;
	p->func2();//12
}

如果想提升代码健壮性,可以这样写:

void func1(){
	if (this == NULL) {
		return ;
	}
	cout<<m_A<<endl;
}

7,const修饰成员函数,表示在此函数内,不可以修改非静态成员变量(属于对象的变量),static类型和加了mutable修饰的变量可以修改,使用const修饰对象,那么此对象只能调用const修饰的成员函数和静态函数(即不允许修改属于这个对象的自己的成员变量)

Person{
public:
	int id_A;
	mutable int id_B;
	static int id_C;
	void func(){       //随便改
		id_A = 10;
		id_B = 10;
		id_C = 10;
	}
	void func2() const {
		id_A = 20;      //报错
		id_B = 20;	   //可以改
		id_C = 20;     //可以改
	}
	static func3(){
		ban = 20;
	}

int Person::id_C = 0;
int main(){
	Person p1;   //随便调用
	const Person p2; //只能调用func2和func3
}

8,友元
大致分三种:
(1)全局函数做友元; 这样写之后全局函数visit可以访问Person的私有属性
(2)类做友元; 这样写之后GoodGay的visit01和visit02都可以访问Person的私有属性
(3)成员函数做友元。 这样写之后GoodGay中只有visit01可以访问Person的私有属性

Person{
public:
	friend void visit();   //(1)全局函数做友元写法
	friend class GoodGay;  //(2)类做友元写法
	friend void Goodgay::visit01(); //(3)成员函数做友元写法
	Person(){
	}
public:
	string name;
private:
	double money;
}

class GoodGay{
public:
	GoodGay(){
	}
public:
	Person p;
	void visit01(){
		cout<< p.name <<endl;
		cout<< p.money <<endl;
	}
	void visit02(){
		cout<< p.name <<endl;
		cout<< p.money <<endl;
	}
}


void visit(Person& p){
	cout<< p.name <<endl;
	cout<< p.money <<endl; //加友元之前报错,加了之后可以访问
}

int main(){
	Person p;
	visit(p);

}

9,运算符重载:分为全局函数运算符重载和成员函数运算符重载,目前只允许单目和双目运算符重载,而且只能用于自定义类型,简单类型如int、double类型不可以重载。
(1)重载+号与左移运算符<<
以+号(双目运算符)为例:
成员函数运算符重载本质上是p1.operator+(p2);因此单目运算符不能有参数,双目运算符只能有一个参数,因为默认+左操作数使用this指针占了一个;而且可以形成函数重载,如果要加多个参数,可以使用类外函数运算符重载,然后加友元;或者直接使用全局函数重载运算符
p1 + p2 全局函数运算符重载本质上是operator+(p1, p2),也是看几目运算符,单目只能有一个参数,双目就只能有两个参数,不能多也不能少,如果只写一个参数 Person operator+(Person &p),那么其应该这么调用:+p1;不知道有啥用

class Person{
public:
	int m_A = 10;
	int m_B = 10;
	Person operator+(Person &p){          //重载运算符+
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}
	Person operator+(int num){            //重载运算符+  同时也与上面的函数形成函数重载
		Person temp;
		temp.m_A = this->m_A + num;
		temp.m_B = this->m_B + num;
		return temp;
	}
}
//如果把成员函数重载运算符注掉,也可以用全局函数重载运算符
Person operator+(Person &p1, Person &p2){
	cout<< "全局函数重载运算符" <<endl;
	Person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
//重载左移运算符<<,cout是对象名,其类型是ostream(标准输出流),全局只能有一个,起名就叫cout,因此必须以引用方式使用(不然做形参或者返回值时会调用拷贝构造函数创建副本),另外cout<<的返回值还是cout,这也是为什么可以链式编程cout<<值1<<值2 ,如果返回值不是cout,那就不能链式编程,只能输出一个值cout<<值1,如果值是peivate的还要加上友元
//由于要求cout必须在左,要输出的值在右,因此必须以全局函数形式重载,因为cout是左操作数,<<是双目运算符,如果是成员函数形式重载
//那么要输出的值是就必须是左操作数,cout是形参,如下
//ostream& operator<<(ostream &cout){},
ostream& operator<<(ostream &cout, Person &p){
	cout<< "m_A: "<< p.m_A << "m_B: " << p.m_B;
	return cout;
}

void test01(){
	Person p1;
	Person p2;
	Person p3 = p1 + p2;  //本质是: p1.operator+(p2),可以简写成p1 + p2;
	Person p4 = p1 + 20;  //本质是:p1.operator+(20),可以简写成p1 + 20;
	
	//把成员函数重载运算符去掉后,使用全局函数重载运算符
	Person p5 = p1 + p2;      //会打印:全局函数重载运算符,本质是:Person p5 = operator+(p1, p2);
	
	cout << p1 <<endl;
}

int main(){
	test01();
	
    system("pause");
    return 1;
}

(2)自增运算符的重载:

class Person{
	friend ostream& operator<<(ostream& cout, Person p);
public:
	//重载前置++  如++p, 这里返回Person引用类型是为了链式编程
	Person& operator++(){   
		this->height++;
		return *this;
	}
	//重载后置++  如p++  这里加个int是为了占位,防止函数重定义,也让编译器识别这是后置++的重载
	//这里返回Person类型而不是引用类型,是因为temp是局部变量,不能返回引用类型,如果开辟在堆区,就可以返回引用类型了
	Person operator++(int){ 
		Person temp = *this;
		this->height++;
		return temp;
	}
private:
	int height = 180;
}
//重载左移运算符<<
ostream& operator<<(ostream& cout, Person p){
	cout << p.height;
}
int main(){
	Person p;
	cout << p++ << endl;   //180
	cout << ++p << endl;   //182
}

这里碰到一个坑,就是如果重载左移运算符时,第二个参数声明为引用类型,那么cout<<p++<<endl;就会报错:

class Person{
	friend ostream& operator<<(ostream& cout, Person& p);
	...
}
ostream& operator<<(ostream& cout, Person& p){
	cout << p.height;
}
int main(){
	Person p;
	cout << p++ << endl;   //报错:二元“<<”: 没有找到接受“Person”类型的右操作数的运算符(或没有可接受的转换
}

不明白为啥报错,难道是因为 p++ 的返回值是个Person而不是Person引用?那之前学习时以下代码为什么不报错?在传参时不是自动将普通类型转成引用类型了吗?请大佬解答

void add(int &a){
	a += 20;
}
int main(){
	int a = 10add(a);
	cout << a << endl; //30
}

得到的答案是:
p++是一个临时对象,在C++中临时对象都是不可修改的,是const Person类型,而引用类型绑定的对象,必须都可以通过该引用来修改,因此报错,就像这样:

int a = 10;
int& b = a;  //成功,a可通过b来修改,例如b = 20

const int c = 10;
int& d = c;  //失败,因为c被const修饰,不可修改,如果要绑定,就必须写成 const int& d = c

这里p++就是一个临时对象,如同上例的c一样被const修饰,不可以直接绑定到引用上,可以加const来绑定:

ostream& operator<<(ostream& cout, const Person& p)

(3)重载赋值运算符=
编译器给每个类默认提供四个函数:
默认构造,默认拷贝构造,默认析构,第四个就是默认的重载赋值运算符=

class Person{
public:
	int* m_A;
	Person(const Person& p){
		cout<<"拷贝构造函数调用"<<endl;
		this->m_A = p.m_A;
	}
	Person& operator=(Person& p){        //重写了 operator=函数,使其变成深拷贝,另外返回值是*this,为了链式编程
		this->m_A = new int(*p.m_A);
		return *this;
	}
	~Person(){
		if(m_A != NULL){
			delete m_A;
			m_A = NULL:
		}
	}
}

int main(){
	Person p1;
	Person p2;  
	p2 = p1;//这句会打印 拷贝构造函数调用,表明是重载的赋值运算符=函数里,其实是调用了拷贝构造函数
	Person p3;
	p3 = p2 = p1;     //operator=返回*this,并且是引用类型,就是为了这样的链式编程
	return 1;
	
}

(4)重载关系运算符==和!=,自定义类型的比较,不多赘述
(5)重载小括号() 这称之为仿函数:

class Person{
public:
	void operator()(){					//第一个小括号是重载的运算符,第二个小括号是参数列表
		cout << "没有参数的仿函数" <<endl;
	}
	void operator()(string str){        
		cout << str << endl;
	}
}
int main(){
	Person p;
	p("abc");   //使用起来非常像个函数,因此称为仿函数,调用第二个
	p();        //调用第一个
	Person()();  //Person()匿名对象,调用第一个
	Person()("efg");  //Person()匿名对象,调用第二个

	return 1;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值