编程语言C/C++(五)

1. 关于友元

- 有些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍阻止一般的访问,这是很方便做到的。例如被
重载的操作符,如输入或输出操作符,经常需要访问类的私有数据成员。

友元(frend)机制允许一个类将对其非公有成员的访问权授予指定的函数或者类,友元的声明以friend开始,它只
能出现在类定义的内部,友元声明可以出现在类中的任何地方:

友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。通常,将友元声明成组地放
在类定义的开始或结尾是个好主意。

1)友元函数
  - 友元函数是指某些虽然不是类成员函数却能够访问类的所有成员的函数。类授予它的友元特别的访问权,这样
  该友元函数就能访问到类中的所有成员。
  
2)友元类
  - 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成
  员),即通过类的对象访问类的所有成员。
  当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。
  
  - 关于友元类的注意事项:
    (1) 友元关系不能被继承。
    
    (2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有
    相应的声明。
    
    (3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类
    中是否有相应的申明。
    
3)友元成员函数
  - 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。当用到友元成
  员函数时,需注意友元声明和友元定义之间的相互依赖,更一般的讲,必须先定义包含成员函数的类,才能将成
  员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。
  
4)友元的小结
  - 在需要允许某些特定的非成员函数访问一个类的私有成员(及受保护成员),而同时仍阻止一般的访问的情况
  下,友元是可用的。
  
  - 优点:
    - 可以灵活地实现需要访问若干类的私有或受保护的成员才能完成的任务;
    
    - 便于与其他不支持类概念的语言(如C语言、汇编等)进行混合编程;
    
    - 通过使用友元函数重载可以更自然地使用C++语言的IO流库。

  - 缺点:
    - 一个类将对其非公有成员的访问权限授予其他函数或者类,会破坏该类的封装性,降低该类的可靠性和
    可维护性。

2. C中printf函数计算参数时是从右到左压栈的

- 输出宽度:
  - 用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义
  的宽度则补以空格或0
  
  - 精度格式符以“.”开头,后跟十进制整数。本项的意义是:如果输出数字,则表示小数的位数;如果输出的是
  字符,则表示输出字符的个数;若实际位数大于所定义的精度数,则截去超过的部分。

3. 关于整型提升

- 在确定共同的目标提升类型之前,编译器将在所有小于int的整型类型上施加一个被称为整型提升的过程。

- 在进行整型提升时类型char、signed char、unsigned char和short int都被提升为类型int。如果机器上
的类型空间足够表示所有unsigned short型的值,
这通常发生在short用半个字而int用一个字的情况下,则unsigned short int也被转换成int,否则它会被提升
为unsigned int。例如已知如下的枚举类型:
  Enum status {bad,ok};
相关联的值是0和1.这两个值可以但不是必须存放在char类型的表示中。当这些值实际上被作为char类型来存储时,
char代表了枚举类型的底层类型,然后status的整型提升将它的底层类型转换为int。

- 一旦整型提升执行完毕,类型比较就又一次开始。
  EG:unsigned char a = 0xa5;
    unsigned char b = ~a>>4+1;
    printf("%d\n",b);
  Answer:250(移位优先级小于算术运算符)

4. 关于关键字mutable

- 在C++程序中,类里面的数据成员加上mutable后,修饰为const的成员函数,就可以修改它了。

5. 关于初始化列表的初始化顺序

- 初始化列表的初始化变量顺序是根据成员变量的声明顺序来执行的,因此需要注意一下成员变量的声明顺序

6. 有关const成员(常量成员)、static成员(静态成员变量)、const static成员(静态常量成员)的初始化:

1、const成员:只能在构造函数后的初始化列表中初始化

2、static成员:初始化在类外,且不加static修饰

3、const static成员:类只有唯一一份拷贝,且数值不能改变。因此,可以在类中声明处初始化,也可以像
static在类外初始化

7. 关于虚继承

- 在class B : public virtual A中,对于class B,由于class B虚继承了class A,同时还拥有自己的虚
函数,那么class B中首先拥有一个vfptr B(虚表指针),指向自己的虚函数表。
可虚继承该如何实现?首先要通过加入一个虚类指针(记vbptr_B_A)来指向其父类,然后还要包含父类的所有内容。

- 虚继承是多重继承中特有的概念。虚基类是为解决多重继承而出现的。如果类D继承自类B和类C,而类B和类C都继
承自类A,因此会出现类D出现了两次A。
为了节省空间,可以将B、C对A的继承定义为虚继承,而A就成了虚基类。

8. String的拷贝构造函数

- 基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应
注意以下事项:
  1)派生类的构造函数应在其初始化表里调用基类的构造函数。

   2)基类与派生类的析构函数应该为虚(即加virtual关键字)。


  3)在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。

- 类String拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是
NULL,而“指针”可以为NULL。

- 类String的赋值函数比构造函数复杂得多,分四步实现:
  (1)第一步,检查自赋值。
  
  (2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
  
  (3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符
  ‘\0’。函数strcpy则连‘\0’一起复制。
  
  (4)第四步,返回本对象的引用,目的是为了实现象a= b =c 这样的链式表达。注意不要将return *this 
  错写成return this。

9. 处理#define的时候,忽略代码的逻辑。不管是在某个函数内,还是在函数外,define都是从定义开始直到文件结尾。

10. 逻辑运算符两侧运算对象的值如果是0,则表示假,非0就表示真,不管其类型是什么。

11. 函数内的静态变量为内部静态变量,函数外定义的静态变量为外部静态变量,外部静态变量不能用extern修饰

12. Char a=’\82’,有一个‘\’,那么首先想到的是转义字符常量,‘\ddd’ 是用八进制数的ASCII码表示一个字符,但是本题中’\82’,有一个8,显然已经不是八进制,

那么这个时候实际上就'\82'中包含3个字符,分别是‘\’,'8','2',赋值时是将字符'2'给了a(char型变量值为
1个字节长度,并从右侧开始运算,)。所以a='2',包含一个字符。

13. 为了避免同一个文件被include多次

- #ifndef的方式依赖于宏名字不能冲突

- #pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的"同一个文件"是指物理上的
一个文件,而不是指内容相同的两个文件。

14. 关于左值和右值

- 非常量引用必须是左值!

- 左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存
在的临时对象。

- 在标准C++语言中,临时量(术语为右值,因其出现在赋值表达式的右边)可以被传给函数,但只能被接受为
const &类型。函数形式参数是临时量,是右值。所以只能被接受为const &类型

- 单个&只能对左值取引用,而两个&&则是对右值取引用。左值通常指的是可以在多行使用,右值通常指的是临时变
量,只能在当前行使用。

15. 函数的返回值可以忽略,但异常不可以忽略

EG:void devide(int a,int b){if(b==0)throw b;~}
#try{divide(10,0);}//试着去捕捉异常
catch(int e){cout << “_”<<endl;}//异常时根据类型进行匹配,对象也是类似的操作
另外,catch(...)为捕获所有异常。

- 如果异常抛到顶层,还没有处理,那么这个时候程序会挂掉。即C++的异常机制是跨函数的,C++的异常是必须处
理的。

16. C++异常

- 接口声明
  - 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,
  例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类型的异常。

  - 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()

  - 一个不抛任何类型异常的函数可声明为:void func()throw()

- 异常对象生命周期
  1)普通类型元素:异常对象catch处理完之后就析构
  
  2)引用类型元素:不用调用拷贝构造,异常对象catch处理完之后就析构
  
  3)指针类型元素:如果是栈上的元素的话,异常对象在catch处理前就析构,因此只能用new在堆上分配,
  抛出,然后catch处理完之后delete。

- 一般自己写的异常类都去继承标准异常类的exception,并重写它的what函数和它的虚析构函数。

17. 关于标准输入流

- ch = cin.get()     读取一个字符
- cin.get(ch)      读取一个字符
- cin.get(str, length)   从缓冲区读取一个字符串
- cin.getline(str, length)  读取一行数据
- cin.ignore()      从缓冲区取走并忽略当前字符(第一个字符)
- cin.ignore(10, ‘\n’)   从缓冲区中取走并忽略十个字符(若遇到’\n’则忽略该字符并结束)
- ch = cin.peek()    偷窥一下缓冲区,并返回第一个字符
- cin.putback(ch) 将字符返回缓冲区

18. 关于C/C++浮点数比较

- 除了可以表示为2的幂次以及整数数乘的浮点数可以准确表示外,其余的数的值都是近似值。例如,1.5可以精确
表示,因为1.5=3*2^(-1);然而3.6却不能精确表示,因为它并不满足这一规则。

19. 关于标准输出流

- Cout.flush()           //刷新缓冲区
- Cout.put(‘h’).put(‘l’)<<endl   //输出字符到缓冲区
- Cout.write(“hello”,len)      //二进制流的输出
- Cout.width(10)          //输出数据宽度设置
- Cout.fill(‘*’)          //改变填充的字符
- Cout.unseft(ios::dec)      //卸载当前默认的10进制输出方式
- Cout.seft(ios::oct)       //八进制输出
//通过控制符实现符号输出
-Cout << hex << setiosflags(ios::showbase) << setw(10) << setfill(‘*’) 
<< setiosflags(ios::left) <<number << endl;

20. 关于ios::binary

- 在创建文件流时,可以显示指定它的打开方式为ios::binary,也就是以二进制方式打开。但是,无论是否指定
二进制方式打开文件,读写的最小单位都是字节。那么,它到底起到什么作用呢?以二进制方式打开与普通打开方式
的区别是什么?

- 实际上,二者最大的区别在于对换行符的处理方式不同。由于历史原因,Windows操作系统是用两个字符(\r\n)
来表示换行符的;而Unix操作系统却是用单个字符(\n)来表示换行符的。因此,在创建文件流时,如果指定了以
ios::binary方式打开,那么换行符就是单字符的;否则,就采用Windows操作系统的双字符。

21. 有符号数和无符号数之间的转换。

- 一般情况:
    长 -> 短:低位对齐,按位复制。
    短 -> 长:符号位扩展。
- 精度提升:
    两个变量运算,表示范围小的变量精度向精度大的变量提升(signed -> unsigned)。

22. 柔性数组

- 编译器会认为这就是一个长度为0的数组,而且会支持对于数组data的越界访问。
  1*. 柔性数组,作为占位符放在结构体末尾,使得结构体的大小动态可变,在声明结构体变量的时候可根据需
  要动态分配内存。
  
  2. 长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量, 数组名这个符号本身代表了
  一个不可修改的地址常量。
  
  3. 常用于网络通信中构造不定长数据包,不会浪费空间浪费网络流量。

23. 从socket读数据时,socket缓存里的数据,可能超过用户缓存的长度,如何处理?

- 第一种方式:可以调用realloc(),扩大原有的缓存块尺寸。但是临时申请内存的有一定性能损失。这种情况要看
接收缓存的方式。使用100k的大接收缓存为例。 如果要等待数据,并进行解析。可能发生缓存不够的情况。此时只
能扩充缓存,或先处理100k的数据,再接收新的数据。

- 第二种方式: 使用缓存队列,分成8K大小的队列。
不存在接收缓存不够的情况。除非用户解析已出错,使用数据接收、使用脱勾。这种方式的代价是,可能需要将缓存
队列再次拷贝、拼接成一块大的缓存,再进行解析。而在本人的系统中,只需要将socket接收的数据再次原样分发给
客户,所以这种方案是最佳方案。

24. C++的虚函数有什么作用?

- 虚函数作用是实现多态,很多人都能理解这一点。但却不会回答下面这一点。

- 更重要的,虚函数其实是实现封装,使得使用者不需要关心实现的细节。在很多设计模式中都是这样用法,例如
Factory、Bridge、Strategy模式。 
前两天在书上刚好看到这个问题,但在面试的时候却没想起来。个人觉得这个问题可以很好的区分C++的理解水平。

25. std::numeric_limit::min 返回int的最小值,std::numeric_limit::max 返回int的最大值