【C++】类和对象(完结篇)

简介: 【C++】类和对象(完结篇)

1. 再谈构造函数

那上一篇文章呢,我们学了类的6个默认成员函数,其中我们第一个学的就是构造函数

那我们先来回忆一下构造函数:

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

也就是说,构造函数其实就是帮我们对类的成员变量赋一个初值

举个栗子:

class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

对于像这样的一个类来说:


虽然经过上述构造函数的调用之后,对象中的成员变量已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋值。

因为初始化只能初始化一次(即在定义的时候赋初值),而构造函数体内可以对成员变量进行多次赋值。

这里注意初始化(定义的时候赋初值)和赋值的区别。

那我们现在来看这样一个类:

class A
{
private:
  int _a1;
  int _a2;
};

问大家一个问题:这里面的int _a1; int _a2;

f5e31295ef314a4caa64ae5d4ce22f53.png是对成员变量_a1、 _a2的声明还是定义?

这里是不是声明啊,只是声明一下A这个类里有这样两个成员变量。

那它们在哪定义呢?

🆗是不是在这个时候:

0bc6a7428c9048f7af554817432ce1f4.png

但是,这里不是对对象整体的定义嘛。

那对象的每个成员变量什么时候定义呢?

可是变量整体定义了的话,它的成员不都也定义了吗?

这些成员不都是属于这个对象的吗?

63f0bfb415f84dc3b8e63baed7b5fd11.png

我们运行也没出什么问题。

道理好像是这样的,但是呢?看这种情况:

73d480701f974607aaa3837290cc5784.png

我们现在给这个类里面再增加一个const的成员变量。

那这时我们再去运行程序a1c91636f4ea4c84857f2fda49b168fb.png

哦豁,发生错误了,这么回事?

为什么会这样呢?

🆗,大家来想一下,const修饰的变量有什么特点:

const修饰的变量必须在定义的时候赋初值(初始化)

而我们现在有对_b进行初始化吗?

是不是没有啊,我们构造函数都没写,那编译器是会默认生成一个,但是,我们知道默认生成的根本就不会对内置类型进行处理。

那我们是不是自己写个构造函数就行了:

3808f6375e0048c1aaef52fb0aff713f.png

但是我们发现还不行,为什么呢?

因为const变量必须是在定义的时候赋初值,而我们上面说了构造函数里面只是对其赋值,并不是初始化。

那大家可能想到了:

之前文章里我们在讲解构造函数的时候说了,C++11不是允许内置类型成员变量在类中声明的时候可以给缺省值嘛

0b1de8254eba490b8f3d67acc02bdb56.png

我们来试一下:

5f13e30e295447e0bd73aad01f578e10.png

🆗,这样确实不报错了。

但是,这是C++11之前才提出来的,那C++11之前呢?

如何解决这样的问题呢?

现在的问题是什么:

6323094728df49ec9eea7f9c59c4baf1.png

_b必须初始化,即在定义的时候赋初值,但是现在是不是没法搞啊,构造函数里只能对其赋值,并不是初始化。

那我们是不是要给成员变量也找一个定义的位置,不然像const这样的成员变量不好处理。


那成员变量的定义到底是在哪里呢?


我们可以认为,对象定义的时候,其成员变量也就定义了,但是一个对象可能有多个成员,在对象定义的地方也没法给某个成员初始化啊。

怎么办?

1.1 初始化列表

那面对上面的问题,我们的祖师爷就要去给成员变量找一个定义的地方,那最终找来找去呢,还是把目标锁定在了构造函数。

在构造函数里面呢又搞了一个东西叫做——初始化列表。


初始化列表:


以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。


举个栗子:


对于上面类中const int _b的初始化我们就可以放在初始化列表进行处理:

class A
{
public:
  A(int a1, int a2, int b)
    :_a1(a1)
    ,_a2(a2)
    ,_b(b)
  {
  }
private:
  int _a1;
  int _a2;
  const int _b;
};
int main()
{
  A a(1, 1, 1);
  return 0;
}

21ead4f5735146308103f049fb037ddd.png

这下我们再运行程序:

df45c119aa5f43ed90ced5aee36313d9.png

就可以了。

当然:

在构造函数体内我们还可以再为成员变量赋值

645fc24cfad4495db115d6f68e029e53.png

注意这里成员_b被const修饰,不能再被赋值了。

然后呢,对于初始化列表,还有一些需要我们注意的地方:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 557bccab7fc54e8b95c00e0ab2a595e5.png
  3. 以下三种类成员变量,必须放在初始化列表位置进行初始化:

引用成员变量

const成员变量

没有默认构造函数的自定义类型成员


首先const成员变量:


我们上面举的例子就是const成员变量,它必须在定义的时候赋初值,所以必须在初始化列表对其进行初始化(定义的时候赋初值),当然C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。


然后还有引用成员变量:


这个我们在之前学习引用的时候就说了:

cd6ca4c26b67430eac55602548c44070.png

引用也必须在定义的时候初始化。


最后就是没有默认构造函数的自定义类型成员:


因为默认生成的构造函数对内置类型不做处理,对自定义类型会去调用它对应的默认构造函数(不需要传参的构造函数都是默认构造函数),所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。


举个栗子:

class B
{
public:
private:
  int _b;
};
class A
{
private:
  int _a1;
  int _a2;
  B _bb;
};
int main()
{
  A a;
  return 0;
}

大家看运行这个程序有问题吗?

a706211c563e4eb6a7fd908c1d3cfeca.png

没有问题,因为对于成员B _bb;来说,会调用它对应的默认构造,类B我们虽然没写构造函数,但是有编译器默认生成的构造函数。

当然如果我们写了不用传参的构造函数,也可以。

但是如果这样:

248de00f4693466f92778766d0e4b720.png

此时类B是不是没有默认构造函数了。

32efdc26206343a7b27325b5ed26aee0.png

那这时就不行了。73a6ed6734c141459888daf990c3ffff.png

eceee3425bdf4c0894cacb0f1383af6c.png

让_bb在初始化列表调用其构造函数进行初始化,这样就可以了。


尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会在初始化列表定义。当然我们说了C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。


成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关


看这个程序:

class A
{
public:
  A(int a)
    :_a1(a)
    , _a2(_a1)
  {}
  void Print() {
    cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;
  int _a1;
};
int main() {
  A aa(1);
  aa.Print();
}

大家思考一下结果是啥?

构造函数参数a接收传过来的1,1先初始化_a1,然后_a1去初始化_a2,所以都是1,是吗?

1869e328d1584d2093e1460b628d4279.png

结果是1和一个随机值。

为什么是这样?

原因就是成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

f29ab413cbec4b9eae99984f2993f9c2.png

所以先初始化_a2,然后是_a1

1126cb839cbd4fddb2b2d126d49cc6d2.png

所以是1和随机值。

1.2 explicit关键字

我们先来看这样一个类:

class A
{
public:
  A(int a)
    :_a1(a)
  {}
private:
  int _a2;
  int _a1;
};

那我们现在想用A这个类去创建对象:

int main() 
{
  A a1(1);
  return 0;
}

这样肯定是可以的,去调它带参的构造函数。

那除此之外呢,其实还可以这样搞

f647f4c8f72b454bb90e95a00fdf5c4e.png

欸,这种写法是怎么回事?

这个地方为什么A a2 = 1;这样也可以,1是一个整型,怎么可以直接去初始化一个类对象呢?

🆗,那要告诉大家的是这里其实是一个隐式类型转换。

就跟我们之前提到的内置类型之间的隐式类型转换转化是一样的,会产生一个临时变量,我们再来回顾一下:9239c56c403947d299f28f184336106f.png

那这里A a2 = 1是如何转换的呢?

这里呢也会产生一个临时变量,这个临时变量就是用1去构造出来的一个A类型的对象,然后再用这个临时对象去拷贝构造我们的a2。

那我们可以来证明一下,是不是如我所说的那样:

我们来再写一个拷贝构造:9739ff4052854a0c9401b673296d2b68.png

注意拷贝构造也是有初始化列表的,因为拷贝构造函数是构造函数的一个重载形式。

那我们现在运行程序,看A a2 = 1是不是先用1调构造函数创建一个临时变量,然后再调拷贝构造构造a2。

如果是那就跟我们上面说的一样了。

2b8db72223c94855b8b60274945cdcd0.png

哦豁,构造确实调了,但是后面没去调拷贝构造啊

是我们上面说的不对吗?


🆗,那其实呢,C++编译器针对自定义类型这种产生临时变量的情况,会进行优化

编译器看到你这里先拿1构造一个对象,然后再去调拷贝构造,有点太费事了,干脆优化成一步,直接拿1去构造我们要创建的对象。

当然,不一定所有的编译器都会优化,但是一般比较新一点的编译器在这里都会优化。


但是呢?口说无凭欸!


你凭什么说这里没有优化的话是会产生临时变量的,说不定人家本来就是直接去构造了呢?

那我们再来看这个代码:

A& c = 10;

这样可以吗?

9ce88d55cab046de8043b7b6d9a14f5f.png

🆗 ,不行直接报错了。

但是:

9c78d2613dd64f458c2b7db3f146e35e.png

加个const就行了。


为什么呢?


🆗,这是不是我们之前在常引用那里讲过的啊:

这里产生了临时变量,而临时变量具有常性,所以我们加了const就行了。

欸,那不是说优化了嘛,但是这里是引用就没优化了,直接拿10去构造一个临时对象,然后c就是这个临时对象的引用,所以只有一步构造,就不用优化了。

所以这里确实是会产生临时变量的,上面那种情况确实是进行了优化。

还有一个点就是,一般来说,C++ 中的临时变量在表达式结束之后 (full expression) 就被会销毁,而这里引用去引用一个临时变量的话会延长它的声明周期的。

那上面说了这么一大堆,想告诉大家的是什么呢?

就是 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,是支持隐式类型转换的(C++98就支持这种语法了)。

82a54498d5be44b7b01d3805095da79e.png

这里就可以这样:

d5e448e498a249ac96d57b5e2dd335d5.png

那如果我们这里不想让它支持类型转换了,有没有什么办法呢?

🆗,这就要用到我们接下来要学的一个关键字——explicit

我们只需在对应得构造函数前面加上explicit关键字:

d30c10e01530459789d88f722df0dd6a.png

然后:

caa3dd8ea40c432f8c4d80e7530347f0.png

这样写就不行了。

🆗,但是呢,我们刚才说的是对于单参数的构造函数是支持这种类型转换的,那多参数的构造函数呢?

6c15cb5956f74f9cb34fdcad0ad0dbe2.png首先我们肯定是可以这样用的:

A a(1, 1);

那这里能不能也像上面那样支持隐式类型转换呢,两个参数的构造函数,那这样去用?

1432f6820796479196ff4c7d6f7935bf.png

那首先要告诉大家C++98是不支持多参数的构造函数进行隐式类型转换的。

不过呢,C++11对这块进行了扩展,使得多参数的构造函数也可以进行隐式类型转换,但是,要这样写:

c3b9c77c35ef42e681b94fe06125467a.png

用一个大括号括起来。

那同样的道理,如果我们不想让这里支持这种类型转换,对于多参数的构造函数,也是在前面加一个explicit关键字:

0fb540aa52b545b98a40f05a215db83b.png

用explicit修饰构造函数,将会禁止构造函数的隐式转换。

2. static 成员

我们来看这样一个面试题:


要求我们实现一个类,并在程序中能够计算出一共创建了多少个类对象。


那大家想一下,可以怎么做?


直接去数行不行啊?

直接数是不是有可能不靠谱啊,因为有些创造类对象的地方是不是会进行优化,这个我们上面刚刚讲过。


那还可以怎么搞呢?


大家想一下,要创建一个类对象,有哪些途径,是不是一定是通过构造函数或者拷贝构造搞出来的。

那我们是不是可以考虑利用构造函数和拷贝构造来计算创建出来的对象个数啊。

class A
{
public:
  A(int a)
  {
  }
  A(const A& aa)
  {
  }
};

假设我们现在创建了这样几个对象:

void func(A a)
{}
int main()
{
  A a1;
  A a2(a1);
  func(a1);
  A a3 = 1;
  return 0;
}

那我们就可以怎么做:

🆗,定义一个全局变量n,初值为0。

d5f9a363befb4c87bbc5cab71289920f.png

然后每次调用构造函数或者拷贝构造创建对象时就让n++,那这样最后n的值是不是就是创建的对象的个数啊。

我们来测试一下:

d2f865c214cc4fb397abb2896ab0a657.png

结果是4,对不对啊。

我们分析一下其实就是4,答案没问题。


但是大家说当前这种方法好吗?


其实是不太好的,为什么?

首先这里我们用了一个全局变量,那首先第一个问题就是它可能会发生命名冲突;其次,全局的话,是不是在哪都能访问它(而C++讲究封装),都可以修改它啊,如果在其它地方不小心++了几次,结果是不是就不准了啊。


那有没有更好一点的方法呢?


那当然是有的。

应该怎么做呢?

🆗,我们把统计个数的这个变量放到类里面,这样它就属于这个类域了,就不会命名冲突了,然后如果不想让它在类外面被访问到,我们可以把它修饰成私有的就行了。


但是:


如果直接放到类里面,作为类的一个成员变量:

1a7012ffd49f40419796b2a59c103984.png

那它是不是就属于对象了,但我们要统计程序中创建对象的个数,这样我们每次创建一个对象n就会定义一次,是不是不行啊。

不能让它属于每个对象,是不是应该让它属于整个类啊。

2.1 静态成员变量

怎么做呢?

在它前面加一个static修饰,让它成为静态成员变量。

那这样它就不再属于某个具体对象了,而是存储在静态区,为所有类对象所共享

66c4a53a528846c6ae9c8b1e00de017b.png

但是我们发现加了static之后报错了,为什么?

因为静态成员变量是不能在这里声明的时候给缺省值的。

非静态成员变量才可以给缺省值。

大家可以想一下嘛,缺省值其实是在什么时候用的,在初始化列表用的,用来初始化对象的成员变量的,而静态成员变量我们说了,它是属于整个类的,被所有对象所共享。


类里面的是声明,那静态成员变量的初始化应该在哪?


🆗,规定静态成员变量的初始化(定义的时候赋初值)一定要在类外,定义时不添加static关键字,类中只是声明。

5e8cfa988e7f42349520335b3169708b.png

但是现在又有一个问题:d06f16f8a411499fa49cf6db2192b2a5.png

我们把它搞成私有的,在外面就不能访问了。

当然如果不加private修饰就可以了:90251eeb4cda4fcf87dc698359e52359.png

另外呢,这里除了指定类域来访问静态成员变量,还可以通过对象去访问:

21eee66075f04e4482229a72cabb5ba1.png

因为它属于整个类,并且被所有对象共享。

还可以这样:6b07f26bcf644c328fbf3be7151d8e54.png

这个问题我们之前是不是说过啊,不能看到->或者.就认为一定存在解引用,还是要根据具体情况进行分析。

当然如果是私有的情况下,这样写是不是统统不行啊:


4a2f6d1c000c4fd990d9f51c0356d9c0.png

那我们就可以写一个Get方法:ea2000c7bddb4591ad39499f622fe4ec.png

aa552eee24f344cd975d7e7a867ad6fb.png

成员函数是需要通过对象去调用的

这样就可以了。

那如果我们的程序是这样的呢?

63fbef59e73f4290a7bf258438e402bd.png

在main函数里面我们根本没有创建对象,那我们还怎么调用Getn函数呢?

难道我们专门在main函数里创建一个对象去调用Getn,然后再把结果减1:

414b0c7ab36941c7b089cfaf15e94151.png

因为main函数里的对象是我们为了调用函数而创建的对象,所以最后要减去。

2.1 静态成员函数

那有没有什么办法可以不通过对象就能调用到Getn函数呢?

那我们就可以把Getn函数搞成静态成员函数,也是在前面加一个static关键字就行了。

caf5154858d04ab1ac61deb946587a80.png

但是静态成员函数有一个特性:静态成员函数没有隐藏的this指针,不能访问任何非静态成员。

因为非静态成员是属于对象的,都是通过this指针去访问的,而静态成员函数是没有this指针的。

a3c8767a172f4aa98913d86d2934325b.png

那它没有this指针,就可以不通过对象调用了,所以现在我们通过指定类域也可以调用到静态的Getn函数。

c406b1a9d39042e58a010f83ac0e0be4.png

当然你还通过对象调用也还是可以的。

那我们现在在加一个东西

991daa8f27be4c9ab685ccb04774624f.png

大家觉得现在结果会多几个对象:

dc92e33641824fc9bcd3d4461a91695a.png

🆗,13个,是不是比之前多了10个啊,因为我们又多定义了一个大小为10 的类对象数组。

2.3 练习

那接下来我们来做个题: link

18f8e90a75b94561821352e10ff06c26.png

7978caa205f4417f8eca6078159452b9.png

这道题呢就是让我们求一个1到n的和,但是要求了一大堆,不让用这,不让用那的。

🆗,那不用就不用呗,其实借助我们刚才学的知识,就可以很巧妙的去解这道题。

怎么做呢?

c0d5b03e1a35491094cfe9cd77a3f570.png

我们自己呢定义这样一个类,两个静态成员变量_i和_sum,分别初始化为0,1。

然后我们调用一次构造函数,就让_sum+=_i,然后_i++,这样第一次+1,第二次+2…

那现在我们要求1+2+3…+n的和,怎么办?

是不是是需要调用n次构造函数,所以,我们直接定义一个大小为n的类对象数组就行了。

class Sum{
public:
    Sum()
    {
        _sum +=_i;
        ++_i;
    }
    static int GetSum()
    {
        return _sum;
    }
private:
    static int _i;
    static int _sum;
};
int Sum::_i=1;
int Sum::_sum=0;
class Solution {
public:
    int Sum_Solution(int n) {
        Sum arr[n];//C99支持变长数组,可以用变量指定数组大小,但不能初始化。
        return Sum::GetSum();
    }
};

37ebc9d49bbc48d2bb5dbe1e7e266cb6.png

这样就行了。


2.4 总结

那最后我们来总结一下:


声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。


特性:


静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,静态成员变量一定要在类外进行初始化

类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

静态成员函数没有隐藏的this指针,不能访问任何非静态成员

静态成员也是类的成员,受public、protected、private 访问限定符的限制


目录
相关文章
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
34 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
31 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
19 1