C++基础知识梳理(学习CPlusPlusThings)

c++

学习链接地址

基础知识(关键字)

1. const

  • 定义常量:const int a=100;
  • 类型检查:

const常量与#define宏定义常量的区别:const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查。
(此处的错误在于 #define 定义的宏常量(假设它不是花括号初始化器列表)同样存在类型。例如若写 #define FOURTY_TWO 42 ,则 FOURTY_TWO 的类型是 int 。具体的类型和各种字面量(整数、浮点、用户定义等)和运算符的结果类型有关。)

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
(此处的错误在于,若用 const 定义常量(类型为整数或枚举,必须以常量表达式初始化),则这种常量在非 odr 式使用(粗略来说是只使用其值)时不需要依赖其身为变量的身份,一定场合下甚至可以不需要定义(譬如作为类的 static 成员对象)。编译器在作为常量处理它时,不会依赖“一份定义”,而是像是立即数一样使用它,它本身可能在机器码中被“拷贝”到多个地方,和 #define 定义的宏常量的结果相同。另一方面, const 定义的常量由于是整数或枚举,所以直接变成机器码上的立即数往往性能更好。)

最后是遗漏的一点:
const 定义的变量只有类型为整数或枚举,且以常量表达式初始化时才能作为常量表达式。其他情况下它只是一个 const 限定的变量,不要将与常量混淆。

  • 防止修改,起保护作用,增加系统的健壮性

  • 可以节省空间,避免不必要的内存分配

    • const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数。
    • const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
  • const对象默认为文件局部变量

    • 注意:非const变量默认为extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
// file1.cpp
int ext
// file2.cpp
#include<iostream>
extern int ext;

//extern_file1.cpp
extern const int ext=12;
//extern_file2.cpp
#include<iostream>
extern const int ext;

未被const修饰的变量不需要extern显式声明!
而const常量需要显式声明extern,并且需要做初始化!
因为常量在定义后就不能被修改,所以定义时必须初始化。

  • 指针与const
const char * a; //指向const对象的指针或者说指向常量的指针。
char const * a; //同上

char * const a; //指向类型对象的const指针。或者说常指针、const指针。
const char * const a; //指向const对象的const指针。

如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
不能使用void*指针保存const对象的地址,必须使用const void*类型的指针保存const对象的地址。
允许把非const对象的地址赋给指向const对象的指针

  • 函数中使用const

const修饰返回值:
__ const int func1();本身无意义,因为参数返回本身就是赋值给其他的变量
__ const int* func2();指针指向的内容不变。
__int *const func2();指针本身不可变。
_
const修饰函数参数
__传递过来的参数及指针本身在函数内不可变,无意义!
__参数指针所指内容为常量不可变:void StringCopy(char *dst, const char *src);
__参数为引用,为了增加效率同时防止修改。void func(const A &a)

  • 类中使用const关键字

__任何不会修改数据成员的函数都应该声明为const类型。
__使用const关键字进行说明的成员函数,称为常成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字进行说明的成员函数不能用来操作常对象。
__对于类中的const成员变量必须通过初始化列表进行初始化

class Apple{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
};
Apple::Apple(int i):apple_number(i){}

__ const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
__ 初始化const常量用初始化列表方式外,也可以通过下面方法:将常量定义与static结合,在外面初始化(const int Apple::apple_number=10;)
__

2. static

  • 静态变量: 函数中的变量,类中的变量

函数中的静态变量:
__当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。
_
类中的静态变量:
__ 由于声明为static的变量只被初始化一次,因为它们在单独的静态存储中分配了空间,因此类中的静态变量**由对象共享。**对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使用构造函数初始化。

  • 静态类的成员: 类对象和类中的函数

类对象为静态
__ 就像变量一样,对象也在声明为static时具有范围,直到程序的生命周期。
_
类中的静态函数
__ 就像类中的静态数据成员或静态变量一样,静态成员函数也不依赖于类的对象。我们被允许使用对象和’.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员。

3. this

  • 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
  • this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
  • 在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this
  • 当参数与成员变量名相同时,如this->n = n (不能写成n = n)。

this在成员函数的开始执行前构造,
在成员的执行结束后清除。
非const成员函数会被解析成const A * const this
const成员函数会被解析成A* const this。
在C++中类和结构是只有一个区别的:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针了。

4. inline(类中内联)

  • 声明时不用加inline关键字,实现时要加内联关键字
int Foo(int x,int y);  // 函数声明
inline int Foo(int x,int y) // 函数定义
{
    return x+y;
}


// 或者这样实现:
inline void A::f1(int x){
}
  • 内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

    • 如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
    • 每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。

    • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联
    • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
#include <iostream>  
using namespace std;
class Base
{
public:
    inline virtual void who()
    {
        cout << "I am Base\n";
    }
    virtual ~Base() {}
};
class Derived : public Base
{
public:
    inline void who()  // 不写inline时隐式内联
    {
        cout << "I am Derived\n";
    }
};

int main()
{
    // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
    Base b;
    b.who();

    // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
    Base *ptr = new Derived();
    ptr->who();

    // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
    delete ptr;
    ptr = nullptr;

    system("pause");
    return 0;
} 

5. sizeof

  • 类的大小的计算:
    • 空类的大小为1字节
    • 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间。
    • 对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
    • 普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小
    • 虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!
    • 虚继承,继承基类的vptr。

6. abstract(纯虚函数和抽象类)

  • 纯虚函数和抽象类
  • __ C++中的纯虚函数(或抽象函数)是我们没有实现的虚函数!我们只需声明它! 通过声明中赋值0来声明纯虚函数!
  • __ virtual void show() = 0; // 纯虚函数
  • __ 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!
  • 实现抽象类
  • __ 抽象类中:在成员函数内可以调用纯虚函数,在构造函数/析构函数内部不能使用纯虚函数。
  • 纯虚函数使一个函数变成抽象类,抽象类中至少包含一个抽象类
  • 抽象类型的指针不能创建抽象类基类的实例,只能创建一个派生出来的实现了纯虚函数的类
  • 派生类如果不实现抽象函数,则也会变成抽象类
  • 抽象类可以有构造函数
// 抽象类
class Base { 
    protected: 
        int x; 
    public: 
        virtual void fun() = 0; 
        Base(int i) { x = i; }  // 构造函数
}; 
// 派生类
class Derived: public Base 
{ 
    int y; 
public: 
    Derived(int i, int j) : Base(i) { y = j; } // 构造函数
    void fun() { cout << "x = " << x << ", y = " << y; }
}; 
  • 构造函数不能是虚函数,而析构函数可以是虚析构函数

7. vptr_vtable(虚拟表)

  • 每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
  • 编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
  • 它使每个类对象的分配一个指针的大小。这也意味着vptr由派生类继承

8. virtual

  • 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。

  • 虚函数中的默认函数:

    • __ 虚函数是动态绑定的,默认参数是静态绑定的。默认参数的使用需要看指针或者应用本身的类型,而不是对象的类型!
  • 静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰

    • __ static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义
    • __ 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
  • 构造函数不可以声明为虚函数。同时除了inline、explicit之外,构造函数不允许使用其它任何关键字。

    • __ 尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
    • __ 我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
  • 析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。

  • 通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。

    • __ 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
    • __ 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
    • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
    • RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类指针或引用来检查这些指针或引用所指的对象的实际派生类型。
    • 在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof,以及C#的as、is运算符类似,C++提供了dynamic_cast函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast,dynamic_cast提供了类型安全检查,是一种基于能力查询(Capability Query)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast。

9. volatile

  • volatile 修饰的变量,在对其进行读写操作时,会引发一些可观测的副作用。而这些可观测的副作用,是由程序之外的因素决定的

  • volatile应用:

    • __ 并行设备的硬件寄存器:如果你对此外部设备进行初始化的过程是必须是顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
    • __ 一个中断服务子程序中访问到的变量:产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
    • __ 多线程应用中被多个任务共享的变量:当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
  • 只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

  • 当一个中断服务子程序修该一个指向一个buffer的指针时,指针可以是volatile

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。

  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)

  • const 可以是 volatile (如只读的状态寄存器),指针可以是 volatile

10. assert(断言)

  • 断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、(C++)中。其作用是如果它的条件返回错误,则终止程序执行。
  • 可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。 # define NDEBUG // 忽略断言

11. bit field(位域)

  • “ 位域 “ 或 “ 位段 “(Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的一个好处是它可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。第二个好处是位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。而这种数据结构的缺点在于,位段实现依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
    • 位域在内存中的布局是与机器有关的
    • 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定
    • 取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域
  • 位域使用:
    • 位域通常使用结构体声明, 该结构声明为每个位域成员设置名称,并决定其宽度:
struct bit_field_name{    
	type member_name : width;
	};
ElementsDescription
bit_field_name位域结构名
type位域成员的类型,必须为 int、signed int 或者 unsigned int 类型
member_name位域成员名
width规定成员所占的位数

例子:

struct _PRCODE
{
	unsigned int code1: 2;
	unsigned int cdde2: 2;
	unsigned int code3: 8;
};
struct _PRCODE prcode;


// 该定义使 `prcode`包含 2 个 2 Bits 位域和 1 个 8 Bits 位域,我们可以使用结构体的成员运算符对其进行赋值
prcode.code1 = 0;
prcode.code2 = 3;
procde.code3 = 102;
  • 位域的大小:C 语言使用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同。 有些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。文章以下均默认 unsigned int 为 32 Bits。
  • 位域的对齐:一个位域成员不允许跨越两个 unsigned int 的边界,如果成员声明的总位数超过了一个 unsigned int 的大小, 那么编辑器会自动移位位域成员,使其按照 unsigned int 的边界对齐。
  • 位域的初始化:位域的初始化与普通结构体初始化的方法相同:struct stuff s1= {20,8,6};
  • 位域的重映射:利用重映射将位域归零:
int* p = (int *) &b1;  // 将 "位域结构体的地址" 映射至 "整形(int*) 的地址" 
*p = 0;                // 清除 s1,将各成员归零

12. extern

  • C++虽然兼容C,但C++文件中函数编译后生成的符号与C语言生成的不同。因为C++支持函数重载,C++函数编译后生成的符号带有函数参数类型的信息,而C则没有。

  • 如果C++中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。此时extern "C"就起作用了:告诉链接器去寻找_add这类的C语言符号,而不是经过C++修饰的符号。

  • C++调用C函数的例子: 引用C的头文件时,需要加extern "C"

//add.h
#ifndef ADD_H
#define ADD_H
int add(int x,int y);
#endif

//add.c
#include "add.h"
int add(int x,int y) {
    return x+y;
}

//add.cpp
#include <iostream>
#include "add.h"

// 添加了extern后
#include <iostream>
using namespace std;
extern "C" {
    #include "add.h"
}
  • 编译的时候一定要注意,先通过gcc生成中间文件add.o。:gcc -c add.c

  • 然后编译:g++ add.cpp add.o -o main

  • 而通常为了C代码能够通用,即既能被C调用,又能被C++调用,头文件通常会有如下写法:

#ifdef __cplusplus
extern "C"{
#endif
int add(int x,int y);
#ifdef __cplusplus
}
#endif
  • C中调用C++函数:
    • extern "C"在C中是语法错误,需要放在C++头文件中。

(1)C++调用C函数:

//xx.h
extern int add(...)

//xx.c
int add(){
    
}

//xx.cpp
extern "C" {
    #include "xx.h"
}

(2)C调用C++函数:

//xx.h
extern "C"{
    int add();
}
//xx.cpp
int add(){
    
}
//xx.c
extern int add();
  • 不过与C++调用C接口不同,C++确实是能够调用编译好的C函数,而这里C调用C++,不过是把C++代码当成C代码编译后调用而已。也就是说,C并不能直接调用C++库函数。

13. struct

  • C中struct:

    • 在C中struct只单纯的用作数据的复合类型,也就是说,在结构体声明中只能将数据成员放在里面,而不能将函数放在里面。
    • 在C结构体声明中不能使用C++访问修饰符,如:public、protected、private 而在C++中可以使用。
    • 在C中定义结构体变量,如果使用了下面定义必须加struct。
    • C的结构体不能继承(没有这一概念)。
    • 若结构体的名字与函数名相同,可以正常运行且正常的调用!例如:可以定义与 struct Base 不冲突的 void Base() {}
  • C++中struct:

    • C++结构体中不仅可以定义数据,还可以定义函数。
    • C++结构体中可以使用访问修饰符,如:public、protected、private 。
    • C++结构体使用可以直接使用不带struct。
    • C++继承
    • 若结构体的名字与函数名相同,可以正常运行且正常的调用!但是定义结构体变量时候只用用带struct的!不适用typedef定义结构体别名
  • C和C++中的Struct区别

CC++
不能将函数放在结构体声明能将函数放在结构体声明
在C结构体声明中不能使用C++访问修饰符。public、protected、private 在C++中可以使用。
在C中定义结构体变量,如果使用了下面定义必须加struct。可以不加struct
结构体不能继承(没有这一概念)。可以继承
若结构体的名字与函数名相同,可以正常运行且正常的调用!若结构体的名字与函数名相同,使用结构体,只能使用带struct定义!

14. struct与class

  • struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
  • 最本质的一个区别就是默认的访问控制
  • 默认的继承访问权限。struct 是 public 的,class 是 private 的。
  • struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

15. union

  • 联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
  • 默认访问控制符为 public
  • 可以含有构造函数、析构函数
  • 不能含有引用类型的成员
  • 不能继承自其他类,不能作为基类
  • 不能含有虚函数
  • 匿名 union 在定义所在作用域可直接访问 union 成员
  • 匿名 union 不能包含 protected 成员或 private 成员
  • 全局匿名联合必须是静态(static)的

16. c实现C++多态

  • C++中的多态:在C++中会维护一张虚函数表,根据赋值兼容规则,我们知道父类的指针或者引用是可以指向子类对象的。如果一个父类的指针或者引用调用父类的虚函数则该父类的指针会在自己的虚函数表中查找自己的函数地址,如果该父类对象的指针或者引用指向的是子类的对象,而且该子类已经重写了父类的虚函数,则该指针会调用子类的已经重写的虚函数。

  • 封装实现:C语言中是没有class类这个概念的,但是有struct结构体,我们可以考虑使用struct来模拟;使用函数指针把属性与方法封装到结构体中。

  • 继承实现:结构体嵌套

  • 多态实现:类与子类方法的函数指针不同,模拟多态,必须保持函数指针变量对齐(在内容上完全一致,而且变量对齐上也完全一致)。否则父类指针指向子类对象,运行崩溃!

17. explicit

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外

18. friend

  • 友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:

    • 友元函数:普通函数对一个访问某个类中的私有或保护成员。
    • 友元类:类A中的成员函数访问类B中的私有或保护成员
    • 优点:提高了程序的运行效率。
    • 缺点:破坏了类的封装性和数据的透明性。
    • 友元关系不可传递
    • 友元关系的单向性
    • 友元声明的形式及数量不受限制
  • 在类声明的任何区域中声明,而定义则在类的外部。

    • friend <类型><友元函数名>(<参数表>);
    • 友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。
  • 友元类:

    • 友元类的声明在该类的声明中,而实现在该类外:friend class <友元类名>;
    • 类B是类A的友元,那么类B可以直接访问A的私有成员。
#include <iostream>

using namespace std;

class A
{
public:
    A(int _a):a(_a){};
    friend class B;
private:
    int a;
};

class B
{
public:
    int getb(A ca) {
        return  ca.a; 
    };
};

int main() 
{
    A a(3);
    B b;
    cout<<b.getb(a)<<endl;
    return 0;
}

19. using

  • 局部using:
void func() 
{
    cout<<"::func"<<endl;
}

namespace ns1 {
    void func()
    {
        cout<<"ns1::func"<<endl; 
    }
}

namespace ns2 {
#ifdef isNs1 
    using ns1::func;    /// ns1中的函数
#elif isGlobal
    using ::func; /// 全局中的函数
#else
    void func() 
    {
        cout<<"other::func"<<endl; 
    }
#endif
}
  • 全局using:using namespace std;
  • 改变访问性:类Derived私有继承了Base,对于它来说成员变量n和成员函数size都是私有的,如果使用了using语句,可以改变他们的可访问性,如上述例子中,size可以按public的权限访问,n可以按protected的权限访问。
class Base{
public:
 std::size_t size() const { return n;  }
protected:
 std::size_t n;
};
class Derived : private Base {
public:
 using Base::size;
protected:
 using Base::n;
};
  • 函数的重载:在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。如果对于基类的重载函数,我们需要在派生类中修改一个,又要让其他的保持可见,必须要重载所有版本,这样十分的繁琐。在派生类中使用using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就行了,而无需为继承而来的其他函数重新定义。
  • 取代typedef:
    • typedef vector V1;
    • using V2 = vector;

20. ::

  • 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
  • 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
  • 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

21. enum

  • 出错:
    • 作用域不受限,会容易引起命名冲突。例如下面无法编译通过的:解决作用域不受限带来的命名冲突问题的一个简单方法是,给枚举变量命名时加前缀
    • 会隐式转换为int
    • 用来表征枚举变量的实际类型不能明确指定,从而无法支持枚举类型的前向声明。

一般说来,为了一致性我们会把所有常量统一加上前缀。但是这样定义枚举变量的代码就显得累赘。C 程序中可能不得不这样做。不过 C++ 程序员恐怕都不喜欢这种方法。替代方案是命名空间:

namespace Color 
{
    enum Type
    {
        RED=15,
        YELLOW,
        BLUE
    };
};

更“有效”的办法是用一个类或结构体来限定其作用域,例如:定义新变量的方法和上面命名空间的相同。不过这样就不用担心类在别处被修改内容。这里用结构体而非类,是因为本身希望这些常量可以公开访问。

struct Color1
{
    enum Type
    {
        RED=102,
        YELLOW,
        BLUE
    };
};
  • C++11 的枚举类:
    • 新的enum的作用域不在是全局的
    • 不能隐式转换成其他类型
/**
 * @brief C++11的枚举类
 * 下面等价于enum class Color2:int
 */
enum class Color2
{
    RED=2,
    YELLOW,
    BLUE
};
r2 c2 = Color2::RED;
cout << static_cast<int>(c2) << endl; //必须转!
  • 可以指定用特定的类型来存储enum
enum class Color3:char;  // 前向声明

// 定义
enum class Color3:char 
{
    RED='r',
    BLUE
};
char c3 = static_cast<char>(Color3::RED);
  • 类中的枚举类型:
    • 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
    • 枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。
class Person{
public:
    typedef enum {
        BOY = 0,
        GIRL
    }SexType;
};
//访问的时候通过,Person::BOY或者Person::GIRL来进行访问。

22. decltype(查询表达式的类型)

  • decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”。
  • 与using/typedef合用,用于定义类型。
  • 这样和auto一样,也提高了代码的可读性。
int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。


using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型

typedef decltype(vec.begin()) vectype;
  • 重用匿名类型:
struct 
{
    int d ;
    double b;
}anon_s;
// 而借助decltype,我们可以重新使用这个匿名的结构体:
decltype(anon_s) as ;//定义了一个上面匿名的结构体
  • 泛型编程中结合auto,用于追踪函数的返回值类型:
template <typename T>
auto multiply(T x, T y)->decltype(x*y)
{
	return x*y;
}
  • 判断规则:
int i = 4;
int arr[5] = { 0 };
int *ptr = arr;
struct S{ double d; }s ;
void Overloaded(int);
void Overloaded(char);//重载的函数
int && RvalRef();
const bool Func(int);

//规则一:推导为其类型
decltype (arr) var1; //int 标记符表达式

decltype (ptr) var2;//int *  标记符表达式

decltype(s.d) var3;//doubel 成员访问表达式

//decltype(Overloaded) var4;//重载函数。编译错误。

//规则二:将亡值。推导为类型的右值引用。

decltype (RvalRef()) var5 = 1;

//规则三:左值,推导为类型的引用。

decltype ((i))var6 = i;     //int&

decltype (true ? i : i) var7 = i; //int&  条件表达式返回左值。

decltype (++i) var8 = i; //int&  ++i返回i的左值。

decltype(arr[5]) var9 = i;//int&. []操作返回左值

decltype(*ptr)var10 = i;//int& *操作返回左值

decltype("hello")var11 = "hello"; //const char(&)[9]  字符串字面常量为左值,且为const左值。


//规则四:以上都不是,则推导为本类型

decltype(1) var12;//const int

decltype(Func(1)) var13=true;//const bool

decltype(i++) var14 = i;//int i++返回右值

23. 引用与指针

引用指针
必须初始化可以不初始化
不能为空可以为空
不能更换目标可以更换目标
  • 一般我们使用const reference参数作为只读形参,这种情况下既可以避免参数拷贝还可以获得与传值参数一样的调用方式。
  • C++提供了重载运算符的功能,我们在重载某些操作符的时候,使用引用型返回值可以获得跟该操作符原来语法相同的调用方式,保持了操作符语义的一致性。一个例子就是operator []操作符,这个操作符一般需要返回一个引用对象,才能正确的被修改。
  • 指针与引用的性能差距:

void test(const vector<int> &data)
{
    //...
}
int main()
{
  	vector<int> data{1,2,3,4,5,6,7,8};
    test(data);
}
  • C++编译器在编译程序的时候将指针和引用编译成了完全一样的机器码。所以C++中的引用只是C++对指针操作的一个“语法糖”,在底层实现时C++编译器实现这两种操作的方法完全相同。
  • C++中引入了引用操作,在对引用的使用加了更多限制条件的情况下,保证了引用使用的安全性和便捷性,还可以保持代码的优雅性。在适合的情况使用适合的操作,引用的使用可以一定程度避免“指针满天飞”的情况,对于提升程序稳定性也有一定的积极意义。最后,指针与引用底层实现都是一样的,不用担心两者的性能差距。

24. 宏

  • 字符串化操作符(#):

    • 在一个宏中的参数前面使用一个#,预处理器会把这个参数转换为一个字符数组,换言之就是:#是“字符串化”的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串
  • 符号连接操作符(##):

    • “##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。将宏定义的多个形参转换成一个实际参数名。
    • (1)当用##连接形参时,##前后的空格可有可无。
    • (2)连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
    • (3)如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开。
  • 续行操作符(\):

    • 当定义的宏不能用一行表达完整时,可以用”\”表示下一行继续此宏的定义。
    • 注意 \ 前留空格。
  • 避免由宏引起的警告:#define EMPTYMICRO do{}while(0)

  • 定义单一的函数块来完成复杂的操作

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值