C++学习笔记(1)——类和对象

考试月这一个月的时间没怎么碰过C++了,再加上本人对这部分内容不太熟悉,说实话挺生疏的,而且协会也要求写学习笔记啊啊啊啊啊啊啊 ,所以这个星期先复习了一下面向对象基础。

零、C++的指针

  • 动态内存分配实例:
//分配出sizeof(int)大小的空间
int *p;
p = new int;
*p = 5;

//分配出10*sizeof(int)大小的空间
int *q;
q = new int[10];
q[4] = 5;
  • 释放动态内存分配实例:
delete p;
delete [] q;
delete q; //出错!
  • 定义常量指针实例:
int a, b;
const int *p = &a;//写成int const也行
*p = 1;//出错!不能通过常量指针修改指向的内容
a = 1;
*p = &b;

int *q;
p = q;
q = p;//出错!这样子的话q可以修改p指向的内容了
  • 定义指针常量实例:
int a;
int *const p = &a;//不能再指向别的地址了!!!
int q;
q = p;
p = q;//出错!

const*不能通过指针修改变量值,*const不能指向别的地址。总之,const*,谁在前面,谁就不允许改变。

一、构造函数&析构函数

构造函数是用来初始化对象的参数的,那么析构函数就是对对象的消亡处理,也就是我们所说的“处理后事” 。个人的习惯是,如果是复杂的构造函数、复制构造函数、析构函数和成员函数都写在类外面,如果写在里面会显得类定义十分混乱。

我们写了一个小程序说明什么是构造函数复制构造函数(也叫做拷贝构造函数)和析构函数,以及详细说明它们在不同作用域的生存周期。搞清楚这些生存周期是至关重要的。

#include<iostream>
using namespace std;

class A{
	public:
		int num;
		//构造函数,若不写的话即为无参构造函数,最好写上吧 
		//当只有一个参数的构造函数是类型转换构造函数,例如下面 
		A(int _num)
		{
			num = _num;
			cout<<"对象"<<num<<" 构造啦"<<endl;
		}
		//复制构造函数,个人认为如果没有这个需求就不要写
		//参数最好加const,防止初始化其他对象时误修改了被复制对象
		//如果不写,系统会调用默认复制构造函数 
		A(const A &a) 
		{
			cout<<"对象"<<a.num<<" 复制然后构造啦"<<endl;
		}
		//析构函数,必须无参 
		~A()
		{
			cout<<"对象"<<num<<" 毁灭啦"<<endl;
		}
};

A a1(1);//定义了一个全局对象

A Test(A a)
{
	cout<<endl<<"Test函数开始噜!"<<endl;
	static A a6(6);//定义了静态对象,与python中的global用法类似
	A a7(7);
	cout<<"Test函数结束噜!"<<endl;
	return a7;
}

int main()
{
	cout<<endl<<"main函数开始噜!"<<endl;
	A a_array[3]= {2, A(3), 4};//这里2和4被转换成临时对象,调用了构造函数
	A a5(5);
	a1 = 100;//100也被转换为一个临时对象,调用了构造函数
	//然后临时对象会复制到对象a1,接着被销毁 
	//注意,只有类型转换构造函数才能这样写
	cout<<"Test函数输出结果:"<<Test(a5).num<<endl<<endl;

	A a8(8);
    a8 = a5;//这里只是简单地复制,不会调用复制构造函数
    cout<<endl;
    
	cout<<"main函数结束噜!"<<endl<<endl;
	return 0;
}

输出结果如下(附上简要说明):

对象1 构造啦//全局对象,首先被构造

main函数开始噜!
对象2 构造啦
对象3 构造啦
对象4 构造啦
对象5 构造啦
对象100 构造啦//100会被转换为一个临时对象,因此会调用构造函数 
对象100 毁灭啦
对象5 复制然后构造啦//Test函数的形参是对象,在函数被调用时,生成的形参要用复制构造函数初始化

Test函数开始噜!
对象6 构造啦
对象7 构造啦
Test函数结束噜!
Test函数输出结果:7
//函数返回值对象的生成原本也是由复制构造函数初始化的
//按道理讲应该还会有个复制过程
//不过Dev C++的编译器出于优化效率,就没有这个步骤了

对象7 毁灭啦//对象7作用域:Test函数
对象1 毁灭啦//唯一的不解之处:这个输出我不知道怎么来的
对象8 构造啦//此处没有调用复制构造函数

main函数结束噜!

对象5 毁灭啦//对象a8消亡
对象5 毁灭啦
对象4 毁灭啦
对象3 毁灭啦
对象2 毁灭啦//对象2~5作用域:main函数,首先被销毁
对象6 毁灭啦//对象6作用域:整个文件,因此最后被销毁
对象100 毁灭啦//对象1作用域:整个文件,因此最后被销毁

注意: 下面这个程序,这是我们经常犯错的地方:

#include <iostream>
using namespace std;
class Sample {
	public:
		int v;
		Sample(int x = 0){ v = x; }
		Sample(const Sample &o){ v = o.v + 2; }
};

void Print(Sample o)
{
	cout << o.v<<endl;
}

int main()
{
	Sample a(5);
	Print(a);
	
	Sample b(a);
	Print(b);
	
	return 0;
}

输出结果不是我们预想的两个5,而是7和9。这是因为函数Print调用了复制构造函数,结果a.v的值就变成了7,b.v的值又在原来基础上加了2,就变成了9。

解决办法:复制构造函数的函数体改成v = o.v;就行了。不要看这个例子很简单,当程序变得很复杂时极有可能疏忽这个小细节。

写了这么多,我想说的是,一般情况下不要自己写复制构造函数,因为这可能会导致某函数体的return返回值与函数的返回值不相同(Dev C++的编译器没有这个问题,但是其他编译器比如VS2010编译器会有),除此之外还会导致函数的实参和形参不相等(这个问题基本上每个编译器都会有)。但是,在一些情况下必须要自己写,举个例子:

#include<iostream>
using namespace std;

class A{
	public:
		int *p;
		A(){ p = new int; }
};

int main()
{
	A a1;
	A a2(a1);
	cout<<a1.p<<" "<<a2.p<<endl;
	return 0;
}

一次的输出结果是:0x396338 0x396338

这意味着a1.pa2.p指向了同一个地址,这是十分危险的! 如果我在析构函数来个delete a1.p,那么a2.p就变成所谓的野指针了。事实上,默认复制构造函数是一种浅拷贝,因此当我们自己写复制函数的时候,就要是深拷贝,写的时候应当重新申请一片内存,再把要拷贝的值拷贝进去(相当于手动操作)。

另外构造函数还可以添加初始化列表:

class A{
	private:
		int sth1;
		int sth2;
	public:
		A(int s1, int s2):sth1(s1), sth2(s2){ ...}
		//等价于 sth1 = s1; sth2 = s2;
};

二、封闭类

一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类。例如下面这个程序,对象A为封闭类对象,对象A1、A2为成员对象。

#include<iostream>
using namespace std;
class A1{
	public:
		A1(){cout<<"对象A1建造"<<endl;}
		~A1(){cout<<"对象A1消亡"<<endl;}
};
class A2{
	public:
		A2(){cout<<"对象A2建造"<<endl;}
		~A2(){cout<<"对象A2消亡"<<endl;}
};
class A{
	private:
		A1 a1;
		A2 a2;
	public:
		A(){cout<<"对象A建造"<<endl;}
		~A(){cout<<"对象A消亡"<<endl;}
};

int main(){
	A a;
	return 0;
}

输出结果如下:

对象A1建造
对象A2建造
对象A建造
对象A消亡
对象A2消亡
对象A1消亡

封闭类对象生成时,先运行所有成员对象的构造函数,然后才执行封闭类自己的构造函数;封闭类对象消亡时,则刚好相反,先执行封闭类的析构函数,然后再执行成员对象的析构函数。

成员对象就相当于零件,封闭类对象就相当于一个完整的产品。零件造好了,完整的产品才能出来;拆解产品时,先将产品上的零件全部拿下来,然后再把零件拆了。

三、友元

友元=友元函数+友元类。

1.友元函数
一旦某个函数被某个类声明为友元函数(加修饰符friend),那么在友元函数内部可以访问该类对象的私有成员,甚至是改变其值。友元函数既可以是类里面的成员函数,也可以不是(即普通函数)。一个友元函数的例子:

#include<iostream>
using namespace std;
class A;//提前声明

class B{
	public:
		void sth2(A a);
};

class A{
	private:
		int v;//私有成员变量!!!
	public:
		friend void sth1(A a);//这两个友元函数随便访问A的私有成员 
		friend void B::sth2(A a);
};

void sth1(A a)//这个函数不属于任何一个类,
//但由于A声明了友元函数,它可以访问A的私有成员 
{
	a.v = 1;
	cout<<"friend! "<<a.v<<endl;
}

void B::sth2(A a)//这个函数属于类B,
//但由于A声明了友元函数,它可以访问A的私有成员
{
	a.v = 2; 
	cout<<"friend!!! "<<a.v<<endl;
}

int main()
{
	A a;
	sth1(a);
	
	B b;
	b.sth2(a);
	
	return 0;
}

输出结果如下:

friend! 1
friend!!! 2

注意,不能把其他类的私有成员函数声明为友元。这就相当于,我和你虽然是朋友(friend),但是我没经过你的允许,我就不能知道你的秘密(private),除非你愿意把秘密公开(public)。

2.友元类
一个类A可以将另一个类B声明为自己的友元。这样类B的所有成员函数就可以访问类A的私有成员了。注意:友元类与封闭类是有区别的。封闭类的成员变量有其他类的对象,这些对象属于封闭类所有;而友元类只是被声明为友元,并不属于被声明的类里面。下面这个程序,类B声明了类A是自己的友元。

#include<iostream>
using namespace std;

class B{
	private:
		int v;
	public:
		friend class A;//声明类A为友元类
};

class A{
	public:
		void Test(B b)
		{
			b.v = 100;//随便访问类B的成员
			cout<<b.v<<endl;
		}
};

int main()
{
	A a;
	B b;
	a.Test(b);
	return 0;
}

输出结果自然就是:100。
注意:在C++中友元不具有传递性。A是B的友元,B是C的友元,就不能推出A是C的友元。友元不是相互的,具有单向性,A声明B是它的友元,B就可以随意访问A,但是反过来A不能随便访问B。

四、静态成员&常量成员

1.静态成员
无论是成员变量还是成员函数,只要前面加了static,它们就变成了静态的了。要注意,静态成员函数不能访问非静态成员变量,也不能调用非静态成员函数。原因在于,非静态成员函数不具体作用于某个对象。下面给出我自己的例子:

#include<iostream>
using namespace std;

class A{
	private:
		int n;
		static int total;//定义了静态成员变量,用于统计总数
	public:
		A(int _n);
		A(const A &a);
		static void Print();//输出总数
};

A::A(int _n)//构造函数
{
	n = _n;
	total += n;
}

A::A(const A &a)//复制构造函数
{
	total += a.n;
}

void A::Print()
{
	cout<<total<<endl;
}

int A::total = 0;//必须在文件中(类定义外)进行声明或初始化

int main()
{
	A num1(2), num2(5);
	A num3(num2);//调用复制构造函数
	num1.Print();//写成num2(or 3).Print()也行
	return 0;
}

输出结果显而易见,是12。

2.常量对象和常量成员函数
常量对象可以执行常量成员函数和静态成员函数,但是不能调用非常量成员函数。原因在于,非常量成员函数内部可能含有修改对象的语句。下面来看一个例子:

#include<iostream>
using namespace std;

class A{
	public:
		void az() const//如果这里去掉const,编译会出错
		{
			cout<<"const"<<endl;
		}
};

int main()
{
	const A a;
	a.az();
	return 0;
}

输出的是:const。

如果某个成员函数中不需要调用非常量成员函数,也不需要修改成员变量的值,那么最好写成常量成员函数,这样不容易出错。

好了,就写到这了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值