文章目录
-
- 1.对象的基本构成
-
- 其他类对象作为变量成员
- 默认构造
- 析构函数
- 其他函数
- 2.构造函数初始化列表,委托构造
- 3.成员初始化顺序
- 4.对象的初始化
- 5.类内的三种属性
- 6.getter和setter函数
- 7.静态成员
- 8.对象指针和对象数组
- 9.对象作为函数参数或返回值
- 10.对象的拷贝
- 11.友元函数
类是抽象的概念,由class进行建立。而对象是类的具体变量。对象是类实例化的结果。
1.对象的基本构成
对象由变量和函数组成。变量作为对象的数据成员,而函数则定义对象可以完成的行为。
函数中有两类特殊的函数。构造函数(constructor)和析构函数(destructor)。分别简写为ctor和dtor。这两类函数为对象所必需之函数。前者用来在生成对象的时候构建变量,后者用来在对象生命期结束的时候销毁对象。正因为它们的重要性,所以即使程序员没有在对象中定义这两类函数,编译器也会自动将其生成。这自动生成的函数称之为默认函数。同时,为了让编译器认出这两种函数,所以构造函数的函数名与类的名字相同,析构函数的名字为类的名字之前加上~。
先举一个构造类的例子。
class Circle {
public:
double radius;
Circle ()
{
radius = 1.0;
}
Circle (double radius_)
{
radius = radius_;
}
Circle (const Circle& c)
{
radius = c.radius;
}
};
这其中radius是变量,三个以Circle命名的函数是三种构造函数。从参数个数可以看出,第一个为无参构造函数,第二个为有参构造函数,第三个则称为拷贝构造函数。无参构造函数为对象的变量赋上之前已经写好的值;有参构造函数则是赋给变量传过来的值radius_;拷贝构造函数利用传来的对象,得到该对象变量的值并且赋给它自己的变量。
其他类对象作为变量成员
其实某个类的变量本身也可以是另外的对象。如下所示
class Circle {
public:
double radius;
Circle ()
{
radius = 1.0;
}
};
class Shape {
Circle c;
Shape () = default;
};
Shape的变量成员就是另一个Circle类对象。需要注意的是Circle必须在Shape之前进行定义。
默认构造
仅无参构造函数和拷贝构造函数具有默认构造形式。拷贝构造函数是真正意义上的不需要程序员自己定义。并且默认拷贝构造函数形式与上述列出的一样。
默认无参构造函数则有些讲究。它是在类的定义中唯一一个必须被写出来的构造函数,不过不必被完整地写出来。具体参见下面的例子。
class Circle {
public:
double radius{ 1.0 };
Circle () {};
};
像上面这样,在无参构造函数中什么都不写,相当于是一个默认构造函数。这时编译器会将上面第三行写的1.0作为radius无参构造函数初始化的值。但在这种情况下,如果第三行没有对radius的值进行初始化并且用的是默认无参构造函数,那么在定义Circle类对象的时候,编译器就会报错“使用了未初始化的变量”。
析构函数
析构函数和构造函数一样,也是必需的。构造函数在对象构造的时候调用,析构函数则是在对象被销毁的时候自动调用。这里不妨先说一下销毁的两种方法。
class Circle{
...
};
int main(){
{
Circle c1{}; //方法一
}
Circle* c2 = new Circle{};
delete c2; //方法二
return 0;
}
方法一是用大括号构建一个作用域,超出这个作用域以后对象就被自动销毁。
方法二是先在堆上建立一个Circle类的指针,然后再用delete销毁。
下面是析构函数定义的一个例子。
class Circle {
public:
double* radius = new double{ 1.0 };
Circle () {};
//析构函数在此
~Circle () {
delete radius;
}
};
析构函数的默认形式和这个差不多。宗旨就是做善后工作,比较明显的例子是他会把堆上面的指针清除,就像是上面这个例子一样。和构造函数一样,可以自己定义析构函数,来对默认析构函数进行覆盖。
析构函数还有一个特点就是析构链,这点会在继承和多态里面说。现在需要知道的是,一旦某个对象的时限已至,生存期到头,那么就会调用它的析构函数。
其他函数
除去构造函数以外,还可以自己定义函数。这里就和普通的函数没有什么太大的差别了,可以自由定制设计自己想要的功能。下面是一个例子,getArea实现了计算圆的面积并将结果返回。
class Circle {
public:
double radius = 1.0;
Circle() = default;
Circle(double newradius) {
radius = newradius;
}
int getArea(){
return (radius * radius * 3.14);
}
};
void main(){
Circle c1;
std::cout << c.getArea(); //获得c1的面积
}
调用方式就是在对象名字后面加“.”,然后正常进行函数调用。
2.构造函数初始化列表,委托构造
先给出构造函数初始化列表的方法:
class Circle {
public:
double radius;
Circle (double radius) : radius{radius} {} //函数一
Circle () : Circle(2.0) {} //函数二
};
上面这个代码有如下几点需要注意和理解:
- 先注意两个构造函数的初始化方式。可以看到大括号里面是空的。它们的初始化操作都在冒号后面的区域里。它被称为初始化列表。尤其注意函数一,尽管形参的名字和它自己的变量的名字都是radius,但是采取初始化列表的时候,重名也没关系,编译器会自动判断的。(当然能不重名还是别重名)
- 按道理的话函数二应该这么写啊:
不过函数二实际上却用了那样一种方式。可以发现,函数二实际上是调用了函数一进行初始化。这叫做委托构造。对于变量比较复杂的对象来说,委托构造可以说是省时省力。
3.成员初始化顺序
到目前为止,已经有三种为变量赋值的方式。
class Circle {
public:
double radius{ 1.0 };
Circle () {} //方式一
Circle (double radius) : radius{radius} //方式二
{
this->radius = radius; //方式三
}
};
this指针是编译器自动生成的,使用目的也是在形参重名的时候仍然为变量赋值。
方式一是就地初始化。即该默认构造函数利用第三行完成就地初始化。
方式二是构造函数的初始化列表。上面刚刚说过。
方式三是在构造函数内为成员赋值。这个就比较好理解了。
值得注意的有以下几点:
-
执行次序: 就地初始化 -> 构造函数初始化列表 -> 在构造函数体中为成员赋值
这里的执行次序表示执行赋值的先后,所以实质上执行最晚的赋值方式会真正完成对成员的赋值。就是谁最晚执行就听谁的。
所以实质上优先级是如下的:在构造函数体中为成员赋值>构造函数初始化列表>就地初始化。
- 若一个成员同时有就地初始化和构造函数列表初始化,则就地初始化语句被忽略不执行。
4.对象的初始化
可以用如下方法定义一个Circle类的对象
Circle c1;
Circle c2{ 2.0 };
c1利用的是无参构造函数进行初始化
c2则利用的是有参构造函数。注意这里要利用{}进行列表初始化,用()是会报错的。
Circle c3 = { 2.0 };
Circle c4{ c3 };
Circle c5 = Circle{ 2.0 };
c3利用的是拷贝列表初始化。
c4利用c3,调用拷贝构造函数进行初始化。
c5创建了一个匿名对象。匿名对象的特点就是不需要进行命名,就是实时地拿过来当一个工具人。接下来同样和c3相似,做一个拷贝列表初始化。
5.类内的三种属性
类有三种属性:private,public,protected。这三种属性可以作用于类的成员。protected主要应用于继承方面,这里暂且不表,主要说public和private。下面是一个例子:
class Circle {
private:
double radius = 1.0;
public:
Circle() = default;
Circle(double newradius) {
radius = newradius;
} //如果把构造函数写在private里面,那么也不能完成对应的初始化
int getArea(){
return (radius * radius * 3.14); //getArea是类里面的函数,所以可以访问radius
}
};
void main(){
Circle c1;
std::cout << c.radius; //错误,private属性的成员无法被外部访问
std::cout << c.getArea(); //正确,public属性的成员可以被外部进行访问
}
private属性的成员不能被类以外的部分访问,而public属性可以被任意访问。值得一提的是,假如不在类的定义中专门写上private,public这种属性来对成员进行专门定义,那么就默认所有成员都是private属性。
其实这几种属性也是为了保护对象的内部成员,防止对象的数据被外面随意篡改。
6.getter和setter函数
对每一个类来说,要兼顾对类的保护性和类本身的开放性。类的变量成员要用private保护起来防止被外部随意篡改,保持封装。同时也要设置途径让外部有手段来通过操作改变变量的值。所以类里面就有两种函数,叫做访问器getter和更改器setter。前者用来获取类的变量成员的值,后者来对其进行修改。具体参见下面的例子。
class Circle {
private:
double radius;
public:
Circle() : radius{ 1.0 } {}
Circle(double radius) : radius{ radius } {}
double getRadius() //getter函数
{
return radius;
}
void setRadius(double newRadius) //setter函数
{
radius = newRadius;
}
};
如上,通过这两个函数可以在保持封装性的基础上,不让用户直接操作radius这个数据的同时也让用户能修改它的值。同样,如果不设置getter和setter函数,并将变量成员放在private里面,那么这个对象就是不可变对象,也就是它其中的数据不能被外界修改。
7.静态成员
可以在类中以static来定义某个变量成员。该变量可以被同一个类不同的对象所共享,且生命期为程序开始直到程序结束。下面是一个例子:
class Circle {
private:
double radius;
static int numOfObjects;
public:
Circle() : radius{ 1.0 } { numOfObjects++; }
Circle(double radius) : radius{ radius } { numOfObjects += 2; }
static int getStatic () //注意下面这两个函数的区别(3)
{
return numOfObjects;
}
int getNoneStatic ()
{
return numOfObjects;
}
};
int Circle::numOfObjects; //注意这一行(1,2)
int main() {
Circle c1{} ;
std::cout << c1.getStatic (); //结果为1
std::cout << c1.getNoneStatic (); //结果为1
std::cout << Circle::getStatic (); //结果为1
std::cout << Circle::getNoneStatic (); //出错(3)
Circle c2{ 2.0 } ;
std::cout << c1.getStatic (); //结果为3
std::cout << c2.getStatic (); //结果为3
return 0;
}
有如下几点需要注意:
- 静态成员在未被明确初始化(为某个值)的时候会自动初始化为零。
- 静态成员要在类的外面被初始化,并且不带static关键字。
- getStatic和getNoneStatic的区别就是它们的返回值,前者是静态而后者非静态。它们的区别主要体现在:前者可以通过c1.getStatic()或者Circle::getStatic()来调用,而后者只能通过c1.getNoneStatic ()来调用,也就是只能由某个已经定义了的类进行访问,。
- 静态变量numOfObjects的值在此例中,只会因类的内部对它的修改而改变。不会被第二次初始化,只会因程序的结束而终结。(我形容不太清,不过通过例子能很容易地看出来)
- 注意必须要加“Circle::”的地方:"int Circle::numOfObjects;“以及"std::cout << Circle::getStatic ();”。一定要加,不然会吃苦头,因为出现这个错误的时候,会在第一行报出“无法解析的外部符号”,很难第一时间找出错误的具体位置。对于要加Circle::的理由,不妨这样想:我同样也可以在Square类里面加上一个numOfObjects,加上getStatic()。你必须澄清是要调出Circle里面的相应成员,所以要加Circle::。
8.对象指针和对象数组
同样的,与其他类型相似,对象也具有数组和指针。下面是例子
Circle* c1 = new Circle{};
Circle* c2 = new Circle{ 2.0 };
Circle c3;
Circle* c4 = &c3;
std::cout << c1->getArea() ;
delete c1 ;
c1 = NULL;
c1,c2分别利用无参、有参构造函数生成了Circle类指针。指针建立在堆上。
c4则是取了c3的地址建立了指针。注意c4不是建立在堆上的,所以用delete进行销毁的话会出错。
对象数组的声明方式更需要注意一些。具体参见下面的例子。
Circle c1[2] = { Circle{1.0}, Circle{} };
Circle c2[2] = { 1.0, {} };
Circle c3[2];
Circle* c4 = new Circle[2]{ Circle{1.0}, Circle{} };
Circle* c5 = new Circle[2]{ 1.0, {} };
Circle* c6 = new Circle[2];
delete[]c4 ; //注意删除堆上的数组的方式,括号括在前面
c4 = NULL;
//不要用delete c4;
c1利用了匿名对象构成了列表初始化数组;
c2利用了隐式构造的匿名对象构成了列表初始化数组;
c3则是比较朴实的初始化方法,同时对象数组里面的对象都是用无参构造函数对成员进行初始化;
c4~c6与上面大致相同,只是利用了new在堆区上创建数组。注意只有在堆上的指针可以被delete删掉,否则会报错。注意不要用delete c4这种方式。这种方式只能释放掉数组中的一个对象。
与其他指针类似,指针使用不当也会造成内存空间的浪费。具体可以参见“对象作为函数参数”。
9.对象作为函数参数或返回值
和其他变量类似,以对象作为函数参数的时候可以传值也可以传引用。具体参见下面的例子:
//传值
void passByValue ( Circle c ){
//do sth...
}
void passByReference ( Circle& c ){
//do sth...
}
void passByPointer ( Circle* c ){
//do sth...
}
void main(){
Circle c;
passByValue(c); //传值
passByReference(&c); //传引用
passByPointer(c); //传指针
}
对象同样可以以值、引用、指针三种形式作为函数的返回值。在返回指针和引用时同样要注意,不要在函数里面新建一个对象并且以指针或引用形式返回。这被称为evil的用法。会多申请内存空间,让地址的生存期超过其函数作用域内。正确做法是尽量在形参中添加一个对象,让函数去操作形参对象的内容并返回该形参。
Circle* evilOne ()
{
Circle* c = new Circle{};
// do sth...
return c;
}
Circle* correctOne( Circle* c ){
//do sth on c ...
return c;
}
Circle& evilTwo (){
Circle c{};
//do sth...
return c;
}
Circle& correctTwo( Circle& c ){
//do sth on c ...
return c;
}
最后要注意一点的是,尽量在作为形参或者函数返回值的对象前面加上const。
10.对象的拷贝
类的拷贝构造分为浅拷贝和深拷贝。主要内容就是利用一个对象初始化另一个同类对象。
对象拷贝实际应用中的示意如下。
//c2为一个已定义的Circle类对象
Circle c1{ c2 };
对象的拷贝是基于对象的拷贝构造函数的。在没有单独为对象声明拷贝构造函数的话,默认的函数只是简单的将被拷贝的对象的所有的对象做一个简单地复制。
以下是一个浅拷贝的例子,这里显式写出的拷贝构造函数与编译器自动生成的默认拷贝构造函数是一样的。首先要注意在为构造函数引入该类Circle类型的变量的时候。必须是以Circle&——引用的形式,否则会报错。
class Circle {
public:
double radius = 1.0;
double* area = new double{2.0};
Circle() = default;
Circle(const Circle& c)
{
this->radius = c.radius;
this->area = c.area;
}
};
以下是一个深拷贝的例子。必须对原有默认拷贝构造函数进行覆盖。
class Circle {
public:
double radius = 1.0;
double* area = new double{2.0};
Circle() = default;
Circle(const Circle& c)
{
this->radius = c.radius;
this->area = new double{*(c.area);
}
};
可以看到,主要区别就是在对于被拷贝对象的指针的处理上。
浅拷贝只是简单地将指针的地址复制过来。后面如果修改两个对象(拷贝和被拷贝者)其一的指针,那么另一个也会受影响。
深拷贝仅仅取原对象的指针的值,然后用这个值生成一个新的指针作为拷贝者的指针变量。后面两个指针再无关系。
最后注意,第一,定义拷贝构造函数的时候的形参必须是const,不然会报错。第二,假设仅仅是写一个这样的语句:
//c2为一个已定义的Circle类对象
Circle c1;
c1 = c2;
这样的话不算是初始化,只是对象拷贝,变量只是会原方原样进行复制。从结果上来说,和浅拷贝一样,无论你为Circle定义什么拷贝构造函数。
11.友元函数
由于类里面的很多成员为了保持封装,要定义成private属性。不过有的时候需要将这些private属性的成员授权给另一个可信的类或者函数进行访问和使用。参考下面的例子:
class Circle{
private:
int radius;
public:
friend class Shape;
freind void getRadius(const Circle& c);
//上面两行声明出谁是友元friend
};
class Shape{
private:
Circle c;
public:
Shape() = default;
int getInfo ()
{
return c.radius; //照常理,Shape里面是不能访问Circle的私有属性成员
}
};
void getRadius(const Circle& c)
{
return c.radius ; //同上,被Circle开了绿灯就是爽,随便访问
}
被开了绿灯的类或者函数都具有对该类私有属性成员的访问权限。
The end of this chapter