C和C++重难点复习笔记(二)【面向对象】

最近两天又有点空闲时间,把《C和C++程序员面试秘笈》中的第六章【C++面向对象】和第七章【C++继承和多态】的内容学习了一下。感觉这两章的内容应该放到一章当中,因为都属于面向对象的内容。

该书前三章的内容总结在了C和C++重难点复习笔记(一)【面向过程】中。

此外还有C和C++重难点复习笔记(三)【泛型编程与STL】


第六章 C++面向对象

1、消息指的是对象间相互联系和相互作用的方式。主要有发送消息的对象接受消息的对象消息传递方法消息内容反馈五部分组成。

2、类特性:抽象、继承、封装、多态、重载

3、class和struct的区别:在C中,struct只能定义成员变量不能定义函数;在C++中,class默认是private的,struct默认是public的,而且class可以用于表示模板类型,struct不行。

4、在调用不带参数的构造函数时不需要加小括号。

5、初始化列表的构造顺序,与变量声明的顺序一致(而不是按照出现在初始化列表中的顺序

6、静态数据成员不存在与程序中其他全局名字冲突的可能性,而且可以是private成员,便于隐藏。(全局对象不行)

7、必须用初始化列表而不是在构造函数中赋值的情况:当类中有const引用 成员变量和基类的构造函数时需要初始化列表。(当然在构造函数体内是可以调用父类的构造函数去创建一个父类对象的,这样合法但是与继承无关,只是一个单独的父类对象)

8、静态变量不能在类内被初始化,应该放在类定义外面初始化。

(比如类test里有一个static int i,那么在类外赋值方法是 在类外 int test::i=1 这样可以)

9、静态成员函数不能访问非静态成员。(因为没有this指针)

10、静态成员同样受private的限制,如果定义为private,类的对象不能直接调用。

11、main函数以外定义一个全局对象,那么在main函数执行以前即会执行该对象的构造函数。(另外:main函数执行以后,可以用atexit函数进行函数注册,使得main结束以后还可以执行别的函数。)

12、C++中的空类默认会产生默认构造函数、拷贝构造函数、析构函数、赋值函数、取址运算符&、const类型的取址运算符&。

此外,如果没有显示的指定构造函数,编译器就会生成默认的无参构造函数和拷贝构造函数。但是如果定义了构造函数,用到了拷贝构造函数但是没有定义,编译器就会生成默认的拷贝构造函数。如果没有用,编译器不会产生默认的拷贝构造函数。

13、构造函数可以被重载,析构函数不能被重载,只能有一个,还不能带参数。

14、函数的默认形参,必须从右往左连续地定义。调用时如果省去某个实参,那么其右侧所有的实参必须全部省去。另外还要注意不会产生模糊语义(比如函数重载,两个函数名字相同,第一个五个参数,其中后三个参数有默认;第二个有两个参数。那么调用的时候如果只传入两个参数,会产生编译错误,编译器分不清你想调用的是哪一个。但是不调用不会报错,因为C++编译器认为潜在的二义性不是一种错误)

15、在构造函数内调用构造函数只是在栈上产生了一个临时对象,对自己本身毫无影响。

16、构造函数explicit关键字:防止隐式转换。比如有两个类的构造函数,形参都是一个int型数据。那么第一个没有加explicit,在定义对象的时候,可以直接Test t1=10 隐式调用构造函数。如果构造函数定义的前面加了explicit,那么只能Test t1(10)这样调用。

17、基类的指针可以指向派生类的对象,派生类的指针不能指向基类的对象。(也就是说老子可以指儿子,儿子不能指老子,大逆不道)派生类中如果有同名函数,如果基类中该函数声明为虚函数,那么指向派生类的基类指针调用这个函数,调用的是派生类中的同名函数。(另外,不能调用派生类中的别的不同名的函数)

18、虚析构函数:基类指针指向一个new出来的派生类对象时,之后如果delete掉这个基类指针指向的空间,那么派生类对象实际上没被析构,导致内存泄露。解决的方法是,把基类的析构函数声明为virtual类型的。

当然还有一种解决方法:

A* p=new B();
delete (B*)p;

19、构造函数的调用顺序:

(1)先按顺序构造类内的静态对象的构造函数(但是只在类内声明,在类外没有实现的不用构造)

(2)再按继承顺序调用虚基类的构造函数。

(3)再按继承顺序调用非虚基类的构造函数。

(4)按照声明顺序调用成员对象的构造函数。

(5)最后调用自身的构造函数。

析构函数的执行顺序与构造函数的执行顺序相反。

20、三种情况会调用复制构造函数(重要):

第一:一个对象以值传递的方式传入函数体

第二:一个对象以值传递的方式从函数内返回

第三:一个对象需要通过另一个对象进行初始化

注意后两个可能会结合,比如Test a=func(),其中func函数返回一个对象,该对象赋值给对象a进行初始化。

(关于这点,书185页显示,Test a=func(),会调用复制构造函数两次。也就是说:func中返回一个临时对象,调用一次;返回的临时对象给a初始化,调用一次。但是实测和书上的并不一致,只调用了一次。很奇妙,究竟是哪次没有用呢?更改代码,这里把Test a=func()改成 func(),也就是说不给任何对象初始化了。那么仍然会显示调用了一次复制构造函数。这说明从函数体内返回一个对象一定会调用拷贝构造函数。难道初始化不用调用?关于这点,上网搜索后,发现大家都有这个疑问。有人说是什么优化机制。书193页面试题28,和185页这道题又矛盾了,显示的和实际一致只调用一次。因此这点是个特例,如果被问到,应当留意,最好是把知识点说明,然后强调一下这种情况实际只会调用一次比较特殊,从而证明你知道知识点,这样不管标准答案是什么都不会扣分。如果是机试题目,还是按一次来)

21、拷贝构造函数形参要用引用&(否则会无限递归下去),而且最好加上const关键字(有的编译器不加会报错)。

原因:例如如果拷贝构造函数长这样:test (test a);那么如果调用test(b)的时候,其实是test (test a=b),然后重复变成了test (test(test(test a=b)))……没完没了的下去。(注意,test a=b 调用的是拷贝构造函数,而不是赋值函数。test a; a=b;这样才是调用赋值函数)

test A=B; //调用的是拷贝构造函数(不是赋值函数)

test A;
A=B; //调用的这才是赋值函数

赋值函数是对于一个已经被初始化的对象进行operator=操作。复制构造是一个对象初始化一块内存区域。

22、什么情况下应该自己定义拷贝构造函数和赋值运算符而不用默认的:

当类中包含动态内存分配的情况。如果不自己定义,用默认的,因为动态分配的内容在堆上,复制该对象时,新旧对象会指向同一个外部内容(即浅复制),那么析构的时候就可能会产生问题,会对同一块堆内存释放两次。

23、继承类的复制函数:

假如基类中有私有成员,继承类如何定义复制构造函数?

Derived(const Derived &obj):Base(obj),j(obj.j)

即使用基类的复制构造函数,(基类中定义一个拷贝构造函数Base(const Base &obj))

否则不能给基类的私有成员传值,因为派生类不可见基类的私有成员。

24、赋值函数返回*this指针,因此函数的返回值应为该类的&引用,形参也应当是引用,否则多调用一次拷贝构造函数。

25、临时对象:

B play(B b)
{
    return b;
}

B t2=play(t1);

如上这种情况,先调用拷贝构造函数,把t1复制给临时对象b,然后临时对象b拷贝给t2,之后b析构。

(也就是说返回的临时对象完成它的拷贝或者赋值功能以后,才会被析构。)

26、构造函数和析构函数是成对的,不过拷贝构造函数也算构造函数,因此应该是拷贝构造函数和构造函数的总数和析构函数一一对应。此外,如果定义静态成员,并且只在构造函数中对静态成员执行+1操作,默认的拷贝构造函数并不会执行+1操作

27、理论上讲,临时变量和局部变量是有区别的。比如,真正的临时对象实际上是看不见的。参数按值传递,返回值按值传递这两种情况下会产生临时对象。这点区别非常重要

Test fun2()
{
    Test t; //局部变量
    return t;
}
t1=fun2();

以上这种情况,之后会析构三次,一次是fun2中的局部变量t,一次是fun2返回的临时变量,一次是拷贝给的t1。因此如果对析构函数加上输出操作,会看到输出三次析构。这三者的地址在内存中实际并不一致

另外,如果函数是Test fun1(Test a),形参如果是像这样不是引用,就会产生临时对象,函数执行完以后临时对象会析构掉。但是如果形参是引用&,则不会产生临时对象,不会多一个析构操作出来。

28、C++支持函数重载,C不支持的原因是C++编译器会对函数名字进行处理,把原来的名字进一步加入函数参数的类型。C不会对函数名进行处理。(这点书中32题交代有误,不会包含返回值类型和显示的数量值)

29、单凭返回值不足以区分重载。另外,同一种类型的形参换成const也不支持重载。另外,重载函数中只有一个可以在之前用extern”C“修饰(否则都用C修饰,都不会被编译器在底层改名,那么岂不是冲突了吗?)

30、重载(overriding)和重写(overloading)的区别:

这点书上翻译不恰当,不应该被翻译成覆写,应该翻译成重写,要和大多数一致。而且开始也交代反了。(虽然从这本书里学到了很多,但我还是要骂他一句破书!)

重载像上一条所述,名字相同,参数列表不同,返回值可以不同。属于语法规则,编译器在编译阶段完成

重写(覆写)是派生类重写基类的虚函数,要求函数名、参数列表、返回值必须和基类一样!否则不能用基类指针调用派生类对象中的同名函数。这个是在运行阶段决定的

31、各种运算符重载。这块书上的内容有错误。比如两道题都说,友元函数是不能访问类的私有成员的。但其实恰恰相反,友元函数中,就是可以访问类的对象的私有和保护成员。(不然还友元个屁?)

运算符重载的格式是:(以Mystring为例)

Mystring & operator+ (Mystring &string)

即返回值是类的引用,形参是类的引用。

另外,如果是友元函数的话,返回值还是类的引用,但是形参变成了两个,左和右。(因为友元函数没有this指针)

具体这块,我在知乎上发现了一个很好的回答:C++中的运算符重载 - Stellaria橙子叶的文章 - 知乎 

此外,还有几点需要注意:

第一,单目运算符没有形参,

Mystring& operator ++();//前置++
Mystring& operator ++(int)//后置++

 第二,输出流重载:

ostream operator << (ostream &out, Mystring &s)

这个最好弄成友元函数,这样就可以像常规的cout<<s这样调用。否则变成了s<<cout,变成了由对象发起调用,不合习惯。

第三,输入流重载:

这个和往常的不一样:

istream& operator>>(istream &in,Mystring &s)
{
    char p[50];
    in.getline(p,50);//从输入流接收最多五十个字符
    s=p;//调用赋值函数
    return in;//输出<<重载的话是返回out
}

记得还是弄成友元函数。

32、对new操作符进行重载:

这个有点难,感觉不太会考。照本宣科:new后面只有一个参数,类的new操作符重载函数声明中有两个形参。

重载new,第一个形参必须是size_t类型,传入的值就是类的大小。

实际中memset函数中的三个参数(a,b,c)意思是将a当前位置后面的c个字节用b替换。

具体在new操作符实现上,a是malloc出来的一个指针,b是重载的new的第二个参数,c是第一个参数。最后返回a。


第七章 C++继承和多态

1、继承是最常见的,老生常谈,这个本身应该是人人都会的,但是我还是总结一下,希望能再加深点记忆。

对于类的对象来说,对象可以访问类的public成员,对象不能访问类的protected成员和private成员。而类内是可以访问自己的全部成员的。

public继承,使得子类继承父类的public和protected成员,继承以后权限依旧保持不变。父类的private成员实际子类是继承了,但是子类不能直接访问。

protected继承,使得子类把父类的public成员继承为自己的protected成员,protected成员依旧继承为protected成员。

private继承,把父类的public成员和protected成员都继承为自己的private成员,这样在子类的类内可以访问,但是子类的对象不能访问,因为父类的两个成员在子类里面已经变成私有成员了。

无论哪种继承,都不能直接访问父类的私有成员。

2、如果两个类的继承关系为私有,编译器不能把派生类对象转换为基类对象(不能赋值,指针也不能指)

3、私有继承和组合(一个类中定义另一个类的对象)的相似点不同点:

相似点:外部都不能进行指针转换,都有一个包含在另一个类中。

不同点:

如果一个类想有若干个另一个类,只能用单一组合;

私有继承可能会引入不必要的多重继承;

私有继承允许访问基类保护成员;

允许重写虚函数;

4、纯虚函数 virtual void func1()=0; 含有纯虚函数的类为抽象类。抽象类不能被实例化,但是可以定义成一个指针

5、多态:两种多态:

一种是编译时的多态,通过重载来实现;

一种是运行时的多态,通过虚成员即重写来实现。

6、虚函数是通过虚函数表(vtbl)实现的。派生类会继承父类的虚函数表,然后将同名函数进行替换,其他的虚函数依次列在虚函数表之后。多重继承的话,会继承多个虚函数表,然后统一替换重名的函数,其他的虚函数依次列在第一个虚函数表的后面。

因此,只要有虚函数,sizeof就会多一个指针,但是不管有几个虚函数也只多一个指针;

另外虚继承(确保顶端父类只有一个)时,有几个虚继承,就会多安排几个指针。

7、构造函数中,虚拟机制不会发生作用。

因为基类的构造函数在派生类的构造函数之前执行,基类构造函数运行的时候,派生类的数据成员还没有被初始化。因此不会执行派生类的同名函数。(类似混合的方式,也是按照这个原则来看)

8、(易错!!)多态传引用和传值的区别:

假如类A有派生类B,两个都有同名函数print,且标为是虚函数;

void print(A a)
{
    a.print();
}

上面传入A的派生类B,访问的仍然是基类中的print()

void print(A &a)
{
    a.print();
}

上面传入A的派生类,访问的是派生类中的print()

传引用才是多态!!

9、(这道题也易错!!)

//基类中:
virtual void g(int i=10){cout<<"Base "<<i<<endl;}

//派生类中:
virtual void g(int i=20){cout<<"Derived "<<i<<endl;}

如果定义基类指针指向派生类对象,输出的结果是:

Derived 10

调用的是派生类中的同名函数,但是值却是基类中的默认值。

原因:参数的值是在编译期就确定了,不是在运行期确定的。因此取的是基类中的默认值。

10、虚继承:之前已经说过,1—2—1类型的继承时,为了保证顶端的类只有一个副本,因此采用虚继承。第二层继承第一层的时候,采用虚继承,格式为:class A :virtual public B

此外,用虚继承的好处在于,顶端的类可以用指针访问最后的类。如果不用虚继承是,左端的1是不能用指针调用右端的1的。

11、如果没有用虚继承,但是用了多重继承,A继承B和C,B和C中都有show函数,那么通过A.B::show()和A.C::show()来区分。

12、(易错)如果1-2-1类型的继承关系第二层采用虚继承,那么定义第三层的类的对象时,首先调用虚拟基类默认的构造函数,然后再调用派生类的构造函数,派生类的构造函数中再次调用的基类构造函数无效

13、为什么要引入抽象基类和纯虚函数?

第一,为了方便使用多态特性。第二,很多情况下基类本身生成对象不合理。(比如狮子老虎可以生成对象,动物直接生成对象不合理)

抽象基类的指针可以两次指向两个派生类,调用其中的同名函数。基类中的纯虚函数可以当做一个接口来看待。

14、虚函数在子类中可以不被重载,但是纯虚函数在子类中必须被实现。

设置虚函数须注意:
1:只有类的成员函数才能说明为虚函数;
2:静态成员函数不能是虚函数;
3:内联函数不能为虚函数;
4:构造函数不能是虚函数;
5:析构函数可以是虚函数,而且通常声明为虚函数。

15、COM:组件对象模型,component object model,是一种跨应用和语言共享二进制代码的方法。

16、COM接口:相当于一组定义了用法的相互联系的函数原型,只不过不能被实现,相当于纯虚函数。而只要提供指向接口方法的指针,COM对象可以用任何语言实现。

17、ActiveX:微软提出的一套基于COM的构件技术标准。

DCOM:分布式组件对象模型。Distribute COM

DLL HELL:指DLL(动态链接库)版本冲突问题。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值