0%

C++语言精要

By Z.H. Fu
https://fuzihaofzh.github.io/blog/
### 几种必考虑的情况 - 只要有除法,就一定要讨论分母为0; - 一定要判断函数输入参数是否合法; - 指针使用时判断是否非0,用完了置为0。

函数压栈顺序

从右往左。
所以

int g = 1;
printf("%d, %d", g, ++g);

结果为2,2

int g = 1;
printf("%d, %d", g, g++);

结果为2,1,说明g++.exe是解析一个参数就加一次,不是解析完一行再加。

条件解析顺序

从左往右,如果当前已经足以判断(&&前面是False或||前面是True),则不会计算后续条件。

boolalpha

函数名称,功能是把bool值显示为true或false。

cout << boolalpha << ( str1==str2 ) << endl;

(int &)

float a = 1.0f
cout<<(int &)a
从a的起始地址开始,读取sizeof(int)长度,当整数输出

类型长度:

类型 长度(字节)
void 1(sizeof是1,但是有warning)
空类 1(和void一样)
bool 1
char 1
short 2
int 4
long 4
float 4
long long 8
double 8
long double 12(32位系统),16(64位系统)
所有指针 4(32位系统),8(64位系统)

整数的存储方式

是倒着存的,如0xFFEEDDF7在内存中为F7 DD EE FF
所以:

unsigned int a = 0xFFEEDDF7;
unsigned char *b = (unsigned char *)&a;
printf("%x",b[0]);

输出为F7

高精度赋值给低精度

产生截断,类似于用低精度的指针直接指向高精度的首位,再赋值给低精度变量unsigned char b = *((unsigned char *)&a)由于整数是倒着存放的,所以产生的是低位截断。

unsigned int a = 0xFFEEDDF7;
unsigned char b = a;
long long c = 0xAABBCCDD11223344;
int d = c;
printf("%x,%x",b,d);

输出为F7,11223344

运算符优先级

逻辑非,按位取反>乘除加减>位移>比较大小>按位与异或或>逻辑运算>赋值运算

消去最低位的1

x=x&(x-1)
该表达式消去了x二进制数中最低端的1,可用于计算x二进制数中有几个1,或者判断x是否是2的N次方。

位运算的方法求平均数

int x = 729;
int y = 271;
cout<<((x&y) + ((x^y)>>1));

输出为500,x&y表示x和y中相同的位,也等于x,y中相同位的平均数(因为他们相同),x^y表示x、y中不同的位相加,>>1表示除以2取平均数。综上,这个表达式就是取两个数的平均数。

只用位运算实现加法

int Add(int a, int b){
if(0 == b)return a;//没有进位则返回
int sum,carry;
sum = a^b;//不考虑进位的加法
carry = (a&b)<<1;//算出要进位的地方,并左移到该加的位置
return Add(sum,carry);//递归相加
}

交换两个数

a^=b
b^=a
a^=b

extern “C”

C++实现重载函数是将这个函数进行改名,在后面加上参数,如foo(int,int)编译后变成_foo_int_int_,用C去调用这个函数会出问题,因此加上extern "C"之后,则编译后会得到一个符合C规范的名字。

用宏获取结构体中某个元素相对于结构体头的偏移地址

# define FIND(struc,e) (size_t)&(((struc*)0)->e)

先将0强制转化为struc*类型,这样,它的首地址就是0,因此偏移量就等于e的绝对地址,再将其转换为int型的即可。

用宏定义一年有多少秒

# define (60 * 60 * 24 * 365)UL

注意后面的UL,以及在宏中,任何变量都应该用括号括起来

用宏定义MIN函数

# define MIN(A,B) ((A)<=(B)?(A):(B))

注意括号

关于const

申明 含义
const int * a; 指向的东西不能变
int const * a; 通上
int * const a; 指针本身不能变
int f()const; 该函数不能改变成员变量
const int * f(); 该函数返回一个指针,不能通过指针来改变指向的变量

总结:

  • 左边的const表示指向的东西不能变,在右边的const表示指针不能再指向其他变量。
  • const成员函数中,若需改变成员变量,则将需要改变的成员变量用mutable关键字修饰。

结构体对齐

结构体为了读取方便,会对数据元素进行对齐,对齐方式就是取这里面最大的数据类型,为每一个数据所占的长度,其他的小的数据不够的话则补齐。
如有一下代码:

struct ST
{
int i;
bool b;
double d;
};

struct ST2
{
int i;
double d;
bool b;
};
cout<<sizeof(ST);//=16
cout<<sizeof(ST2);//=24
cout<<endl;
cout<<FIND(ST,i)<<FIND(ST,b)<<FIND(ST,d)<<sizeof(ST);

输出
16 24
0 4 8 16
这里,double是最长的元素,所以对齐长度取8,其中bool型的被压缩在i没填完的地方里面了。可见,一个结构体中元素出现顺序不一样,结构体的大小不一样。

|i|i|i|i|b| | | |
|—|—|—|—|
|d|d|d|d|d|d|d|d|

# pragma pack(n)可以更改对齐方式,如果n=1n=1的话,那么尽量按1对齐,如果大于1,如double、int等,那么就按它本身大小对齐。

堆与栈

一个由c/C++编译的程序占用的内存分为以下几个部分 :

1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;

2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵;

3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放 ;

4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放 ;

5、程序代码区―存放函数体的二进制代码。
详见:
http://zhidao.baidu.com/link?url=ejKPzQ9sYih1Q3sYirWst8L1JmYVGopR6WQrfu2_c5zpAbdgoOvpxfzdTOYusNKUG7k9UqVnCM9jy2BxTXskk_

内存分配

malloc:值分配内存,返回的只有void指针,失败返回0
calloc:分配内存,并全部置0;参数为两个,一个是大小一个是多少
new:自动执行构造函数,自动计算内存大小,是运算符而不是函数,失败时抛出异常。

关于sizeof的一些补充

char * p = "123456";
char a[] = "123456";
cout<<sizeof(p);
cout<<sizeof(*p);
cout<<sizeof(a);
cout<<sizeof(*a);

输出4 1 7 1
1)注意sizeof一个数组名,得到的是数组占用空间大小,这是数组和指针的区别之一!

2)sizeof 是运算符不是函数,因此可以sizeof一个类型,如果是变量的话可以不加括号!sizeof a``sizeof (int)

3)sizeof 的返回类型是size_t,是一个与机器有关的无符号整型

4)数组传给sizeof的时候不退化,传给strlen时,退化为指针`,实际上,数组在传给任意函数之后都退化成了指针,在子函数中,不知道数组大小,需要在传一个参数进去。

5)sizeof 类似于一个特殊的宏,在编译时会将所有的变量、表达式替换为其相应的类型,因此,在程序运行过程中,sizeof括号里的标的是得不到执行。sizeof(a=4);a的值是不会改变的,同理sizeof(f())的f()也得不到执行。

6)sizeof不能用于计算动态数组的大小

虚函数

作用:用基类指针去调用子类函数,实现多态。
实现方法:虚函数表,在一个有虚函数的类的开头有虚函数指针,里面指向了一个虚函数的列表如图:

对于多重继承就会有多个虚函数表的指针,就像这样:

因此对于一个没有虚函数的类,sizeof(B)=1,对于单继承的有虚函数的类,sizeof(D)=4,而对于多继承则sizeof(D)=4*n
一般来讲,虚函数指针放在对象的第一个位置,因此其的地址就是类的地址。我们可以通过函数指针强行指向类的第一个指针指向的函数地址,来实现调用,这种方法可以直接调用private函数,是C++的一个bug。
详见 http://blog.csdn.net/haoel/article/details/1948051/

内联函数

编译器会将代码直接替换到调用的地方,会做检查,而宏则不会。inline应与函数定义放在一起,而不是声明。内联函数常用于一些大型工程。

指针

指针和数组名的区别

指针占空间,数组名不占空间

char * c="hello";//"hello"分配在常量区
char c[]="hello";//"hello"分配在栈上这个c不占空间,sizeof(c)
得到的是数组长度。我怀疑是编译之后直接用一个常地址给替换了

const char * strA()
{
    char c[] = "hello";//"hello"分配在栈上,程序结束即消除,
        //所以这种写法错误
    return c;
}
const char * strA()
{
    static char c[] = "hello";//"hello"分配在常量和全局区,
        //返回之后不会消失,合法
    return c;
}

指针减法

会自动除以一个sizeof(当前类型)

用派生类指针指向基类

一般我们用基类指针指向派生类,实现多态,
而用派生类指针指向基类是非法的,如果非要这么做,则需使用dynamic_cast

Derived *d = dynamic_cast<Derived *>( new Base());

main函数返回值

如果没写,则编译器自动返回0,所有函数都必须有返回值。

一些指针

指针类型 写法 备注
函数指针 void (*pf)() 声明时一定要加类型void (*pf)(int,int)=&f;
函数返回指针 void *f()
const指针 const int * p 指向的东西不能变,指向const int a,因此直接加了个*
指向const的指针 int * const p p本身的值不能变,而指向的东西可以变
指向const的const指针 const int * const p 上面两个合起来

一些指针示例

语句 含义
float(**def)[10] def是一个二级指针,指向一个指向一维数组的指针,一位数组元素都是float
double*(*gh)[10] gh是一个指针,指向一个一维数组,数组元素都是double *
double(*f[10])() f是一个数组,有10个元素,元素都是函数指针,指向返回double型的无参数的函数
int*((*b)[10]) int*(*b[10])相同,是指向一维数组的指针
long (*fun)(int) 函数指针
int(*(*f)(int,int))(int) f是一个函数指针,指向一个以两个int为参数,返回一个函数指针的函数,返回的函数输入一个int输出一个int
int * a[10] 一个数组,有10个int* 元素
int (*a)[10] 一个指针,指向一个有10个int元素的数组
int * (*p)[10]为例说明判断方式,先找到变量,这是一个指针,然后看最左边,表示的是数据类型是指向整型的指针,右边表示是指向数组的指针。综上,是一个指向一位数组的指针,一位数组里都是int *

数组名

数组名的值是个指针常量,《C和指针》p142中说到,在以下两中场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度。 &a产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。所以&a后返回的指针便是指向数组的指针,跟a(一个指向a[0]的指针)在指针的类型上是有区别的。
定义char a[] = “hello”;

语句 含义 sizeof cout
a 常指针,指向a[0] 6 hello
a+1 常指针,指向a[1] 4 ello
*a 取a的第0个元素 1 h
&a 获得一个指向数组的指针变量 4 0x22ff0a
&a+1 指向首地址加上数组长度的那个元素,已经超出数组范围 - 22ff10
*&a 获得上面指针所指示的数组 6 hello

代码:

char a[] = "hello";
 cout << sizeof(a)<<" "<< sizeof(&a)<<" "<< sizeof(*a)<<" "<< sizeof(*&a)<<" ";
 cout << (a)<<" "<< (&a)<<" "<< (*a)<<" "<< (*&a)<<" ";

输出

6 4 1 6 hello 0x22ff0a h hello

总的来说,a是一个指向数组第0个元素的指针,长度为1,而&为指向数组的指针,长度为n,指示在sizeof(a)的时候变为n,比较特殊。

指针和句柄

句柄是系统提供的系统资源虚拟地址,类似于指针,但是windows会经常将空闲对象释放,需要用时重新加载,因此用物理地址就没法找到,因此需要引入指针来管理。

智能指针

1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想,而且极容易出错。不能复制,因此其管理的对象不能放入 std::vector 等容器中。

2、在确定对象无需共享的情况下,使用 boost::scoped_ptr(当然动态数组使用boost::scoped_array)。

3、在对象需要共享的情况下,使用 boost::shared_ptr(当然动态数组使用boost::shared_array)。

4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下,使用boost::weak_ptr,一般常用于软件框架设计中。

5、最后一点,也是要求最苛刻一点:在你的代码中,不要出现 delete 关键字(或 C 语言的free 函数),因为可以用智能指针去管理。

来源: http://blog.csdn.net/xt_xiaotian/article/details/5714477

this指针

C++的this指针和Python一样,其实是成员函数的一个隐藏参数,而在成员函数中,亦是通过this指针来区别不同的对象。静态成员函数没有this指针。

STL

浅拷贝

含有指针的对象,如果只进行浅拷贝,即直接复制的话,会引发指针重复析构的问题。因此,含有指针的对象最好不要放到容器中,或者定义拷贝构造函数,使用深拷贝。

面向对象

面向对象五个基本原则

  1. 单一职责(Single-Responsibility Principle):就一个类而言,应该仅有一个引起它变化的原因。防止原因间相互交叉。
  2. 开放封闭原则(Open-Closed Principle):是说软件实体(类、模块、函数等等)应该可以扩展的,但是不可修改。
  3. 依赖倒置原则(Dependency-Inversion Principle):抽象不应该依赖细节,细节应该依赖于抽象。
  4. 里氏替换原则(Liskov-Substituent Principe.):子类必须能够替换掉它们的父类。其意思:子类必须具有父类的所有特性
  5. 接口隔离原则(Interface-Segregation Principle):多个专用接口优于一个单一的通用接口。其意思:不要将所有的方法都添加到一个接口中。

编译器默认生成的四个成员函数

构造函数、析构函数、拷贝构造函数、赋值构造函数。

其中

  • 赋值构造函数就是重载等号;
  • 拷贝构造函数用于生成新对象的时候,此时,即使是用A a1=a,调用的也是拷贝构造函数,不会调用重载等号。

生成类对象要不要加括号

如果构造函数有参数,显然要加括号,但是,如果构造函数没有参数,则不能加括号,加了就变成函数声明了。
例如:

A a(1);
B b;
B b();//错误,这是声明b是一个返回值为B类型的函数。

静态成员

  • 静态成员变量:非const型static变量只能在类声明外初始化。

  • 静态成员函数:可以直接使用A::f()的形式调用,不用生成对象。

    class A
    {
    public:
    const static int a = 1;//const static可以直接初始化
    static int b;
    static int f();
    };
    int A::b = 2;//初始化,不能加static
    int main()
    {

    }

初始化列表

  • 在构造函数的定义中指定;

  • 带参数对象(成员对象、基类对象)、引用成员、const只能在初始化列表中初始化;

  • 初始化顺序由成员定义的顺序决定,跟初始化列表里的顺序无关(考点)。

    class A
    {
    public:
    A(int i):b(i++),a(i++),c(0){}
    int a;
    int b;
    const int c;
    };
    int main()
    {
    A t(1);
    cout<<t.a<<" “<<t.b<<” “<<t.c<<” ";
    }

打印 1 2 0

成员变量初始化位置

类型 初始化位置
const 构造函数初始化列表
static 在类外定义
const static 在声明时定义
class A
{
public:
	static int a;
	const int b;
	const static int c = 0;
	A():b(0){}
};
int A::a = 0;

构造函数和析构函数

构造函数 析构函数
private 防止类被实例化,实现单例模式;或者只提供一些工具 只在堆上生成实例,如果在栈上生成实例,函数退出,会有自动析构,编译不通过。需自己添加析构函数Destory()
virtual 禁止这种写法。因为对象还没构造好,不能动态绑定 要实现多态,需采用虚析构函数,保证用delete基函数指针时,能调用派生类的析构函数

继承和多态

继承实现了代码重用
多态实现了接口重用

覆盖(override)和重载(overload)

覆盖通过虚函数的方式
重载通过不同参数的方式

异常

  • 构造函数中抛出异常时,程序会按构造顺序,逆序析构已经构造的变量;
  • 析构函数最好不要抛出异常,因为会导致资源无法释放;
  • 最好不要直接使用指针,因为在构造函数构造抛出异常时,无法自动释放指针,最好用智能指针。

虚函数

虚函数使用的成员变量视传入他自己的this指针而定。也就是说,是哪一个类的虚函数,就调用哪一个类的变量。

继承规则

下表表示不同的基类成员类型在不同继承方式下,在子类里面的访问类型。注意基类的private和变成子类private的区别,基类是private,外界和子类都不能访问,变成子类private类型,子类可以访问,外界不能访问。

继承方式\成员类型 public protected private
public public protected 不能访问
protected protected protected 不能访问
private private private 不能访问

多重继承

多重继承能节省空间,避免冲突。

A       A             A
 \     /             / \
  B   C       →     B   C
   \ /               \ /
    D                 D

声明方式:

class A;
class B:public virtual A;
class C:public virtual A;
class D:public B,public C;

如果B,C中有同名变量,则需在D中用B::a,C::a区别。
虚继承中,每个父类都保留了自己的虚函数表,普通继承中总共只有一个虚函数表。

纯虚函数

纯虚函数定义为virtual int f()=0,含有纯虚函数的类不能实例化对象。子类如果没有将基类的纯虚函数重写完,那么它仍然含有纯虚函数,仍然不能被实例化。