Home

cuipf blog

10 Nov 2018

基础总结-C++基础

类的静态变量

  • C++98中,在类声明中使用’=’初始化变量只能是静态常量(static const),而且必须是整形(int)或者枚举类型
  • C++11中,在类的声明中初始化,添加了对非静态成员初始化的支持,包括使用’=‘以及’{}‘来初始化;但是对于静态常量类型还是只支持int和枚举;
  • 对于非常量的静态成员变量,C++ 11 和 C++ 98保持一致,程序还是需要到头文件以外的地方定义,保证编译时候,类静态成员的定义只存在于一个目标文件中。

C++中易混淆的概念

函数指针和指针函数

函数指针的重点是指针。表示的是一个指针,它指向的是一个函数:

int (*pf)(); 

指针函数的重点是函数。表示的是一个函数,它的返回值是指针:

int* fun(); 

指针数组和数组指针

  • 指针数组: 首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
    int *P1[10];
    
  • 数组指针: 首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
    int (*p2)[10];
    

类模板和模板类

  • 类模板:表示一个模板,专门用于产生类的模子; ``` template class Vector {

}

* 模板类:表示由模板产生而来的类;

vecto, vector都是模板类

注意:根据这对定义很容易推到出*函数模板*和*模板函数*的区别;这里不再赘述;

### 常量指针和指针常量
“*”(指针)和 const(常量) 谁在前先读谁 ;*象征着地址,const象征着内容;谁在前面谁就不允许改变。
* **常量指针:** 常量指针p1:指向的地址可以变,但内容不可以重新赋值,内容的改变只能通过修改地址指向后变换。  
    p1 = &a是正确的,但 *p1 = a是错误的。 

int const *p1 = &b; //const 在前,定义为常量指针

* **指针常量:** 指针常量p2:指向的地址不可以重新赋值,但内容可以改变,必须初始化,地址跟随一生。

    p2= &a是错误的,而*p2 = a 是正确的。

int *const p2 = &c; // *在前,定义为指针常量


## (* p)++ 和 *(p++)和 *p++的区别
1. (*p)++ 先取得指针P的值,然后对其值做加加;
2. *(p++) 先对指针P进行++,即指针地址++,然后取值;
3.  `*p++`,同 `*(p++)`,优先级*(取地址)和 ++是同级,所以都是从右到左;即先加加,再*;
4. `*++p`等价于*(++p);

运算符优先级如下:
运算符优先级一样的时候,执行顺序是**从右到左**;

![运算符优先级](http://cuipf0823.github.io/images/%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7.png) 

## 关于运算符++ 和 --相关说明
对于++、--运算符,我们应注意以下几点:

* ++ 、--运算符只能用于变量,而不能用于常量或表达式,例如8++,(x+y)--均是不合法的。

* ++ 、--运算符是单目运算符,优先级高于双目基本算术运算符,而低于括号( ( ) )运算符,结合性为从右到左。

* ++ 、--运算符的运算对象为字符型、整型、指针型变量或数组元素,运算结果的数据类型同运算对象的类型一致。

* 当出现难以区分的若干个“+”或“-”组成运算符串时,C语言规定:从左到右取尽可能多的符号组成运算符。例如,设整型变量a、b的值均为5,则:
a+++b   应理解为(a++)+b,结果为10,运算后a为6,b不变。

a---b   应理解为(a--)-b,结果为0,运算后a为4,b不变。

但如果出现如下形式:

(a++)+(a++)+(a++)

表达式的值是多少呢?有的系统按照从左到右顺序求解括弧内的运算,求完第一个括弧的值后,实现a的自加,a值变为6,再依次求第二个、第三个括弧的值,结果表达式相当于5+6+7,即18。而另一些系统(如Turbo c)把5作为表达式中所有a的值,因此3个a相加,得表达式的值为15,在求出整个表达式的值后再实现自加3次,a的值变为8。应该避免出现这种岐义,程序中尽量不要出现这种现象。

* 在printf( )函数中,实参数的求值顺序,各系统不一样。在多数系统中对函数的求值顺序是从右到左。例如,i初值为5。

printf("%d,%d",i,i++);

输出结果为:6,5

* 请不要在一个表达式中对同一个变量进行多次诸如i++或++i等运算,例如写成:i++*i++-++i,这种表达式不仅可读性差,而且不同的编译系统对这样的表达式将作不同的处理,因而得到的结果也各不相同。

## strcpy的实现
strcpy注意会拷贝最后的“\0”;

代码实现:

//C语言标准库函数strcpy的一种典型的工业级的最简实现。

//返回值:目标串的地址。

//对于出现异常的情况ANSI-C99标准并未定义,故由实现者决定返回值,通常为NULL。

//参数:des为目标字符串,source为原字符串。

char* strcpy(char* des,const char* source)   {   char* r=des;

assert((des != NULL) && (source != NULL));

 while((*r++ = *source++)!=’\0’);

 return des;   }  

//while((des++=source++));的解释:赋值表达式返回左操作数,所以在赋值’\0’后,循环停止。

## sizeof 和 strlen

char str[20]=”0123456789”; int a=strlen(str); //a=10;strlen 计算字符串的长度,以\0’为字符串结束标记。 int b=sizeof(str); //b=20;sizeof 计算的则是分配的数组str[20] 所占的内存空间的大小,不受里面存储的内容影响

1. sizeof()是运算符,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等;
2. strlen()是函数,要在运行时才能计算。参数必须是字符型指针(const char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
3. sizeof 获得保证能容纳实现所建立的最大对象的字节大小。由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小;
4. 具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
    * 数组——编译时分配的数组空间大小;
    * 指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4\8);
    * 类型——该类型所占的空间大小;
    * 对象——对象的实际占用空间大小;
    * 函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
5. strlen返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL。

## 操作符重载
1. 重载运算符的参数数量和该运算符作用的运算对象数量一样多;一元运算符有一个参数,二元运算符有两个参数;
2. 对于二元运算符,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数;
3. 除了重载operator()之外,其他的运算符重载不能包含默认实参;
4. 如果运算符重载是类成员,则它的第一个运算对象绑定到隐式的this指针上,成员运算符的显示参数数量比运算符运算对象少一个;
5. 复合赋值运算符有+=、-=、>>=等等;
6. 重载运算符调用问题,下面调用等价:

//一个非成员运算符函数的等价调用 data1 + data2 operator+(data1, data2)

data1 += data2; data1.operator+=(data2)


***不可重载运算符和可重载运算符***

![operator](http://cuipf0823.github.io/images/%E5%8F%AF%E4%BB%A5%E9%87%8D%E8%BD%BD%E4%B8%8D%E5%8F%AF%E9%87%8D%E8%BD%BD.png)

## 使用与内置类型一样的含义
* 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致;
* 如果类的操作需要检查相等性,则需要定义operator==;如果有了operator==,意味则也就有了operator!=;
* 同理,类有了operator<,则它也应该含有其他关系操作;
* 重载运算符的返回类型通常情况下应该与其他内置版本的返回类型兼容;逻辑运算符和关系运算符应该返回bool,算术运算符号应该返回一个类型的值;赋值运算符和复合运算符则应该返回左侧运算符对象的一个引用;

## 运算符重载选择作为成员还是非成员
有些运算符必须作为类的成员,而另外一些情况下,运算符作为普通函数更好;可以参考一下准则:
* 赋值(=),下标([]),调用(())和成员访问箭头(->)必须是类成员;
* 复合赋值运算符一般来说应该是类成员,但是非必须;
* 改变运算状态的运算符或给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员;
* 具有对称性的运算符可能转换成任意一端的运算对象,例如:算术、相等性、关系和位运算符等,应该是普通的非成员函数;
    说明:
    ```
    string str = "hello";
    string t = str + "world";   //如果重载为类成员函数,const char* 加到一个string没有问题
    string tt = "world" + str;  //但是,顺序变化一下,重载为类成员马上不行
    ```
* 输入输出运算符重载必须是非成员函数;

## 输入输出运算符重载

/*

  • 输出运算符重载
    1. 形参一需要向其写入内容,又因为我们无法复制一个ostream对象,所以为非常量引用;
    1. 输出运算符尽量的减少格式化操作; */ std::ostream& operator «(std::ostream& os, const String& str) { os « str.c_str(); return os; }

/*

  • 输入运算符重载 */ std::istream& operator»(std::istream& is, String& str) { char temp[100] = { 0 }; is » temp; if (is) { str.data_ = new char[strlen(temp) + 1]; assert(str.data_ != nullptr); strcpy(str.data_, temp); str.size_ = strlen(temp); } else { //输入失败赋值为空 str = String(); } return is; }
**注释:**

因为访问了类String的私有变量,需要把输入输出声明为类的友元;

friend std::istream& operator»(std::istream& is, String& str); friend std::ostream& operator«(std::ostream& os, const String& str);


## 算术和关系运算符重载

通常情况下,把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。

又因为运算符一般不需要改变运算对象的状态,所以形参都是常量引用;

**特别要注意:** 我们一般使用算术运算符计算的结果都会赋给一个临时变量,如果这是我们返回的是引用后果可想而知;

/*

  • 运算符重载 / String operator+(const String& lhs, const String &rhs) { size_t length = lhs.size() + rhs.size(); char temp = new char[length + 1]; assert(temp != nullptr); strcpy(temp, lhs.c_str()); strcpy(temp + lhs.size(), rhs.c_str()); return String(temp, length); } ```

递增和递减运算符

//前置运算符
StrBlobPtr& operator++();
StrBlobPtr& operator--();
//后置运算符 因为做完加加只有,原值已经变了,所以不能返回引用;
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);

成员访问运算符,解引用等重载

class DInt
{
public:
	DInt(int i):imem_(i)
	{

	}
	//prefix increment 如果想要阻止++++i这种行为就返回const DInt& 即可
	DInt& operator++()
	{
		++(this->imem_);
		return *this;
	}
    //prefix decrement
	DInt& operator--()
	{
		--(this->imem_);
		return *this;
	}
	//postfix increment 如果想要阻止i++++这种行为就返回const DInt 即可
    DInt operator++(int)
    {
        DInt temp = *this;
        ++(*this);
        return temp;
    }
    //postfix decrement
    DInt operator--(int)
    {
        DInt temp = *this;
        --(*this);
        return temp;
    }
    //dereference
    int& operator*() const
    {
        return (int&)(imem_);
    }
    int* operator->() const
    {
        return & this->operator*();
    }
    const int& imem() const
    {
        return imem_;
    }
    //operator ()
    void operator()()
    {
        std::cout << imem_ << std::endl;
    }
    friend std::ostream& operator<<(std::ostream& os, DInt i);
private:
	int imem_;
};
std::ostream& operator<<(std::ostream& os, DInt i)
{
    os << *i << std::endl;
    return os;
}

union 联合

  • 联合(union)是一种特殊的类,一个union可以有多个数据成员;但是在任意时刻只有一个数据成员可以有值;
  • 分配给union对象的某个成员赋值之后,该union的其他成员就变成未定义状态;
  • 分配给union对象的存储空间至少要能容纳它的最大的数据成员。

union是一种节省空间的类

类的某些特性对于union同样适用,如下:

  1. union不能含有引用类型的成员,除此之外它的成员可以是绝大多数类型;
  2. 在C++新标准中,含有构造函数和析构函数的类类型也可以作为union的成员类型;
  3. union也可以指定public、private、protected;
  4. 默认情况下,是public这一点和struct一样;
  5. union可以包含构造和析构在内的成员函数,但是union不能继承自其它类,也不能作为基类;所以union不能包含虚函数;

匿名union

union
{
    char cval;
    int ival;
    double dval;
}

cval = 'c';
ival = 42;

在匿名union的定义所在的作用域内核union的成员都是可以直接访问的;

含有类类型成员的union

  • 当union包含的是内置类型的成员时;可以使用普通的赋值语句改变union保存的值;
  • 当union包含特殊类类型成员的时候;如果我们将union的值改为该类类型成员对应的值,或者将类类型成员的值改为一个其他值,都必须运行该类型的构造函数;
  • 当union包含内置类型的成员时;编辑器会依次合成默认构造函数或者拷贝控制成员; 但是如果是含有类类型成员,并且该类型自定义了默认构造函数或者拷贝控制函数,则编译器将为union合成对应的版本并将其声明为”=delete”; ``` union UnDemo { int index; std::string str; UnDemo() { index = 1; } ~UnDemo() {

    } };

//声明中构造析构是必须的 否则报错,因为union包含类类型string 所以默认的构造和析构=delete UnDemo demo;


## 位域
1. 类可以将其非静态数据成员定义为位域(bit-field);
2. 位域的类型必须是整形或枚举类型;因为带符号位域的行为是由具体的实现确定,所以通常情况下我们使用无符号类型保存一个位域;
3. 取地址符号(&)不能用于位域,因此任何指针都无法指向类的位域;

class FileBit { public: typedef unsigned int BIT; BIT mode : 2; BIT modified : 1; BIT prot_owner : 3; BIT prot_group : 3; }; //输出为4 std::cout « sizeof(FileBit) « std::endl;

**说明:** 上面的4个位域被压缩存放在同一个unsigned int中;

## volatile
关键字volatile告诉编译器不应对这样的对象进行优化;volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错

volatile限定符的用法和const相似;它起到对类型额外修饰的作用;

* 一个变量既可以是const 也可以是volatile;
* 只有volatile的成员函数才能被volatile的对象调用;

//volatile volatile int v = 0; volatile int* vip = nullptr; //vip是一个指针,指向volatile; int* volatile ivp = nullptr; //ivp是一个volatile指针,指向int //vivip是一个volatile指针,指向一个volatile整数; volatile int* volatile vivip = nullptr;

int* ip = &v; //错误 必须指向volatile ivp = &v; //错误 必须指向int vip = &v;
vivip = &v;

* const 和 volatile一个重要区别是,volatile不能够使用合成的拷贝/移动构造函数或者赋值运算符号初始化volatile对象或者从volatile对象赋值;

## 运行期和编译期
**编译期多态**:也称为静态多态,主要是通过模板实现;
**运行时多态**:主要是通过虚函数,依附于集成体系实现;

### 编译期

* 编译期是指把你的源程序交给编译器编译的过程,最终目的是得到obj文件,链接后生成可执行文件;
* 编译期为静态或者全局变量分配内存;
* inline 在编译阶段,会将调用动作用调用函数的文本替换;

### 运行期

* 运行期指的是你将可执行文件交给操作系统(输入文件名,回车)执行、直到程序执行结束的期;
* 在堆上分配内存之类都是在运行期进行的;
* virtual function 的确定都是运行期的事情;

## new 的用法

//关于new的使用 //1. 申请空间 调用构造函数 TestNew* p_new = new TestNew(9);

//2.仅仅赋值用,调用构造函数 void* p_new1 = malloc(sizeof(TestNew)); ::new (static_cast<void*>(p_new1)) TestNew(100); free(p_new1);

//3.仅仅用于申请空间 TestNew* start_free = static_cast<TestNew*>(::operator new(100)); delete start_free;


## 必须使用initialization list初始化
* 当初始化一个reference member时;
* 当初始化一个const member时;
* 当调用一个base class的constructor,而它拥有一组参数时;
* 当调用一个member class的constructor,而它拥有一组参数时;


## typeid运算符
* typeid 运算符表达式的形式是typeid(e);typeid操作的结果是一个常量对象的引用;
* typeid运算符可以用于任意类型的表示式,顶层的const被忽略;如果表达式是一个引用,则typeid返回该引用对象的类型;
* 如果是数组或者函数,所得到的结果是数组类型而非指针类型;
* 如果运算对象不属于类类型或者是一个不包含任何虚函数的类型,typeid运算符指示的是运算对象的静态类型;
* 如果运算对象定义了至少一个虚函数,typeid的结果知道运行时才会求得;

Derived dp = new Derived; Base *bp = dp; //运行时比较两个对象的类型 if(typeid(bp) == typeid(*dp)) { //两者指向的是同一个类型 }

if(typeid(*bp) == typeid(Derived)) { //bp实际指向Derived对象 }


## 隐式的类类型转换

**转换构造函数:**
如果构造函数只接受一个实参, 则它实际上定义了转换为此类类型的隐式转换机制, 有时我们把这种构造函数称为转换构造函数;

抑制构造函数定义的隐式转换使用关键字: **explicit**;

### explicit
* 关键字explicit只对一个实参的构造函数有效; 需要多个实参的构造函数不能用于执行隐式转换, 所以无需将这些构造函数指定为explicit; 
* 只能在类内声明构造函数时使用explicit, 在类外定义时不应重复;
* explicit构造函数只能用于直接初始化;
    > 发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=), 此时, 我们只能使用直接初始化而不能使用explicit构造函数:
//ok 直接初始化
Sales_data item1(null_book);
// error 不能将explicit构造函数用于拷贝形式的初始化过程
Sales_data item2 = null_book;

//实例2
class TestDemo1
{
public:
    explicit TestDemo1(int a)
    {
	    a_ = a;
	    cout << a_ << endl;
    }
private:
    int a_;
};
TestDemo1 demo1 = 22; //error
TestDemo1 demo2('f'); //ok ```

cuipf

scribble