考试月这一个月的时间没怎么碰过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.p
、a2.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。
如果某个成员函数中不需要调用非常量成员函数,也不需要修改成员变量的值,那么最好写成常量成员函数,这样不容易出错。
好了,就写到这了。