C++基础查漏补缺

C++基础知识

C++ 中,四个与类型转换相关的关键字?

const_cast、static_cast、dynamic_cast、reinterpret_cast

  1. const_cast

    强制去掉const或 volatile(多线程使用)

  2. static_cast 静态转换 (类型相关)

    运算符完成C++内置基本数据相关类型之间的转换。默认整数和char之间。

  3. reinterpret_cast (类型不相关)

    处理互不相关类型之间的转换,从整型到指针,从一种类型的指针到另一种类型。

    int指针到char指针

  4. dynamic_cast 动态转换

    处理基类到派生类型的转换(下行),给了NXobject,转到line,body等

    也可以在类层次间进行上行转换,从派生类转为基类,此时,dynamic_cast和static_cast相同。

    从基类指针获得派生类行为最好的方法是通过虚函数。当使用虚函数的时候,编译器自动根据对象的实际类型选择正确的函数。但是,在某些情况下,不可能使用虚函数。这时候就需要使用dynamic_cast关键字了。但是,能用虚函数还是用虚函数最好。

    与其他强制类型转换不同,dynamic_cast涉及运行时类型检查。如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。

补充

在上面四个类型转化关键字中,除了static_cast,其他的三个都有可能涉及到指针的类型转换。从本质上来说,指针的类型不同,并没有产生很大的差异,他们都是需要足够的内存来存放一个机器地址。“指向不同类型之各指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。

所以,转换(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存大小和其内容”的解释方式。

类型在内存中所占的空间?

类对象的空间

sizeof一个空的类型,结果是1。

理由:不包含任何信息,本来应该是0,但是声明实例时,内存占用空间,由编译器决定。VS是1。添加构造和析构函数不影响。

如果析构函数为虚函数:编译器发现类型中有虚函数,就会为该类型生成虚函数表,并为每个实例 添加指向虚函数表的指针。在32位机器上,指针占4字节,64位机器,指针占8字节。

数组的空间

注意sizeof(arr),当arr为数组名称时,获取的是数组的实际大小,不是指针,虽然数组名类似于指针。

int tmpGetSize(int arr[])
{
	return sizeof(arr);
}

void testArraySize()
{
	int arr[6] = { 0,1,2,3,4,5 };
	int size1 = sizeof(arr); //int 4  , 4*6=24

	int *p = arr;
	int size2 = sizeof(p); //pointer, in x64, 8

	int size3 = tmpGetSize(arr);//pointer, in x64, 8

	cout << "size of arr, p, and arr parameter:" 
		<< size1 << "," << size2 << "," << size3 << endl;
	//24 , 8, 8
}

形参和实参的区别

形参是函数定义时说明,其规定了函数所接受数据的类型和数量。
实参是函数调用时的输入,实参数量与类型和形参一样,用于初始化形参。

当形参是引用类型时,对应的实参被引用传递,引用形参是实参的别名。
当形参是普通类型时,对应的实参被值传递,形参和实参相互独立。

C++中,建议使用引用类型的形参替代指针,因为使用引用,形式上更简单,无须额外声明指针变量,也避免了拷贝指针的值。如果函数无须改变引用形参的值,最好将其声明为const引用。

复制构造函数

复制构造函数必须是引用,不然由于形参的值传递会无限递归调用复制构造函数导致栈溢出。

一般采用常量引用比较好(const MyClass& b)

class MyClass 
	: public BaseClass
{
public:
	MyClass() {}
	MyClass(const MyClass& b) { data = b.data; }
	//MyClass(MyClass b) { data = b.data; }  //会报错,复制构造函数必须是引用,不然会无限递归调用导致栈溢出
private:
	int data;
};

delete默认函数

在默认情况下(用户没有定义,但是也没有显示的删除),编译器会自动隐式生成一个拷贝构造函数和赋值运算符,但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。

Person(const Person& p) = delete;

常量字符串的地址

直接将指针赋值给相同的常量字符串时,会指向相同的内存地址。

用相同的字符串给数组赋值,则因为数组各自开辟了空间,指向不同的内存地址。

void testConstArray()
{
	char str1[] = "hello world";
	char str2[] = "hello world";

	if (str1 == str2)
		cout << "str1 = str2." << endl;
	else
		cout << "str1 != str2." << endl;  //this is right

	char *p1 = "hello world";
	char *p2 = "hello world";

	if (p1 == p2)
		cout << "p1 = p2." << endl; //this is right
	else
		cout << "p1 != p2." << endl;
}

函数指针

Q21剑指offer里面有一个示例,看看

变量初始化顺序

成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。这点在EffectiveC++中有详细介绍。

class TestMemInit
{
private:
	int n1;
	int n2;
public:
	TestMemInit() : n2(0), n1(n2 + 2) {}
	void printSelf()
	{
		cout << "n1: " << n1 << " n2:" << n2 << endl;
	}
};

// 执行TestMemInit().printSelf() 
//输出为: -87987678(随机数), 0

如果不使用初始化列表,则变量初始化顺序与其在构造函数中的顺序有关。

  • 类中成员在定义时,是不可以初始化的
  • const成员变量必须在构造函数的初始化列表中初始化
  • static变量必须在类外进行初始化

多态中,成员变量的初始化顺序是:

  1. 基类的静态变量或全局变量
  2. 派生类的静态变量或全局变量
  3. 基类的成员变量
  4. 派生类的成员变量

静态变量进行初始化顺序是基类的静态变量先初始化,然后是它的派生类。直到所有的静态变量都被初始化。这里需要注意全局变量和静态变量的初始化是不分次序的。这也不难理解,其实静态变量和全局变量都被放在公共内存区。可以把静态变量理解为带有“作用域”的全局变量。在一切初始化工作结束后,main函数会被调用,如果某个类的构造函数被执行,那么首先基类的成员变量会被初始化。

const 修饰

https://blog.csdn.net/qq_41175905/article/details/81877675

修饰指针:

  • 如果const在*的左侧,则const是用来修饰指针指向的变量,即指针指向的地址中存放的的值为常量;
  • 如果const在*的右侧,则const是用来修饰指针本身,指针本身是常量,其指向固定。

修饰成员函数与变量:

  • const 的类成员变量,只能在初始化列表中赋初值
  • int func() const; 常成员函数,不能修改类内的数据成员。
  • 类的const 对象,只能调用常成员函数??
  • const int func(); 返回值为常量。
const int a = 2;
int b = 1;

const int* p; //指向常量的指针,指向地址内的值不可变
p = &a;       //ok
// *p = 10;   //error

int* const p2 = &b; //p2为常量指针,其指向的地址不可变,需要初值
// p2 = &a;        //error,p2的指向不可变
*p2 = 10;          //b=10,改变b的值
cout << "b:" << b << endl;
// 类
class A
{
private:
    const int a;                // 常对象成员,只能在初始化列表赋值

public:
    // 构造函数
    A() : a(0) { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数、更新常成员变量
    const A a;                  // 常对象,只能调用常成员函数
    const A *p = &a;            // 指针变量,指向常对象
    const A &q = a;             // 指向常对象的引用

    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
    char* const p3 = greeting;          // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
    const char* const p4 = greeting;    // 自身是常量的指针,指向字符数组常量
}

// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数指针所指内容为常量
void function3(char* const Var);         // 参数指针为常量
void function4(const int& Var);          // 引用参数在函数内为常量

// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

static

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员

C 中string操作的函数

  1. strcpy(des, source);
  2. strlen()
  3. strcmp() ??

volatile的作用

  • 防止变量被编译器优化
  • 加 volatile 关键字的变量,从内存中取值,而不是寄存器。

volatile关键字是防止在共享的空间发生读取的错误。只保证其可见性,不保证原子性;使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化

在单任务环境中,如果在两次读取变量之间不改变变量的值,编译器就会发生优化,会将RAM中的值赋值到寄存器中;由于访问寄存器的效率要高于RAM,所以在需要读取变量时,直接CPU寄存器中获取变量的值,而不是从RAM中。

在多任务环境中,虽然在两次读取变量之间不改变变量的值,在一些情况下变量的值还是会发生改变,比如在发生中断程序或者有其他的线程。这时候如果编译器优化,依旧从寄存器中获取变量的值,修改的值就得不到及时的响应(在RAM还未将新的值赋值给寄存器,就已经获取到寄存器的值)。

要想防止编译器优化,就需要在声明变量时加volatile关键字,加关键字后,就在RAM中读取变量的值,而不是直接在寄存器中取值

pragma pack(n)

https://www.cnblogs.com/flyinggod/p/8343478.html

设定结构体、union及类成员变量以n字节的方式对齐

对齐方式:1,2,4,8,16

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GSoWdQLQ-1584275142585)(C++基础查漏补缺.assets/1220093-20180124203608740-485914586.png)]

只有long和指针在不同平台上的大小不同

#pragma pack(push)  // 保存对齐状态
#pragma pack(4)     // 设定为 4 字节对齐

struct test
{
    char m1;
    double m4;
    int m3;
};

#pragma pack(pop)   // 恢复对齐状态

C++ struct 和 class

struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

  • struct: 默认继承和访问权限是 public。
  • class: 默认继承和数据访问控制是 private。

union

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。

成员变量共享一块内存。

explicit

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
struct A
{
    A(int) { }
    operator bool() const { return true; }
};

struct B
{
    explicit B(int) {}
    explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
    A a1(1);        // OK:直接初始化
    A a2 = 1;        // OK:复制初始化
    A a3{ 1 };        // OK:直接列表初始化
    A a4 = { 1 };        // OK:复制列表初始化
    A a5 = (A)1;        // OK:允许 static_cast 的显式转换 
    doA(1);            // OK:允许从 int 到 A 的隐式转换
    if (a1);        // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a6(a1);        // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a7 = a1;        // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a8 = static_cast<bool>(a1);  // OK :static_cast 进行直接初始化

    B b1(1);        // OK:直接初始化
    B b2 = 1;        // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
    B b3{ 1 };        // OK:直接列表初始化
    B b4 = { 1 };        // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
    B b5 = (B)1;        // OK:允许 static_cast 的显式转换
    doB(1);            // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
    if (b1);        // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b6(b1);        // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b7 = b1;        // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
    bool b8 = static_cast<bool>(b1);  // OK:static_cast 进行直接初始化

    return 0;
}

friend友元函数与友元类

  • 能访问私有成员
  • 破坏封装性
  • 友元关系不可传递,假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。
  • 友元关系的单向性,B是A的友元,但是A不是B的友元
  • 友元声明的形式及数量不受限制
class A
{
public:
	A(int _a):a(_a){};
 
	friend int getA_a(A &_classA);//友元函数
    friend class C; //什么友元类 
private:
	int a;
};
 //友元函数定义
int getA_a(A &_classA)
{
	return _classA.a;//通过对象名访问私有变量
}
class C
{
    public:
    int getA_a(A classa){
        return classA.a; //返回A的私有变量
    }
}

using的使用

using 指示 使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了,如 using namespace std

尽量少使用using 指示(会污染命名空间),而使用using 声明,如下:

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;

::范围解析运算符

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

enum

限定作用域的枚举类型

enum class open_modes { input, output, append };

不限定作用域的枚举类型

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };

decltype

decltype 关键字用于检查实体的声明类型或表达式的类型及值分类???

https://zh.cppreference.com/w/cpp/language/decltype

左值和右值

a=10;

左值是变量的地址,右值是变量存储的内容。

变量本质即存储空间的名称,编译后变为对应地址。

引用

引用本质上是一个隐式指针,为对象的一个别名,通过操作符 & 来实现。C++11又提出了左值引用与右值引用的概念,一般如没有特殊说明,提到引用都是指传统的左值引用。

https://blog.csdn.net/thisinnocence/article/details/23883483

左值引用

一个C++引用声明后必须被初始化,否则编译不过,初始化之后就相当与一个变量(地址为初始化时所引用变量的地址)。由于拥有共同的地址,而且也是同一个类型,所以对其操作就相当于对原对象的操作,用法和普通变量相同。所以,引用一般也称别名。

引用与指针最大的区别:指针是一种数据类型,而引用不是。当其用作函数传参时,传递的就是变量的左值即地址。

右值引用

C++11新特性,目的是:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;
  • 能够更简洁明确地定义泛型函数;

右值引用形式:类型 && a= 被引用的对象。与左值的区别在于:右值是临时变量,如函数返回值,且不变。右值引用可以理解为右值的引用,右值初始化后临时变量消失。

#include <iostream>
using namespace std;
int glo=10;
void process(int && a){
	glo+=a;
 
}
 
void process(int &a){
	glo-=a;
}
 
int get_return(){
	int b=3;
	return b;
}
 
int main() {
	int a = 10;
	process(8);
	cout<<glo<<endl; //18
	process(a);
	cout<<glo<<endl; //8
	int && k = 	get_return();
	cout<<k<<endl; //3
	return 0;
}

成员初始化列表

  • 高效:对于非内置类型,减少了一次调用默认构造函数的过程
  • 有些场合必须使用:
    1. const 常量成员,因为不能赋值,必须在初始化列表里面初始化。
    2. 引用类型,必须在定义时初始化。
    3. 没有默认构造函数的类类型,因为不需要调用默认构造函数。

##initializer_list 列表初始化

???

OOP面向对象

OOP,object-oriented programming三大特征:

  1. 封装,private ,protected, public
  2. 继承,父类 ,派生子类
  3. 多态

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  • public 成员:可以被任意实体访问
  • protected 成员:只允许被子类及本类的成员函数访问
  • private 成员:只允许被本类的成员函数、友元类或友元函数访问

多态

  • 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
  • 多态是以封装和继承为基础的。
  • C++ 多态分类及实现:
    1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
    2. 子类型多态(Subtype Polymorphism,运行期):虚函数
    3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
    4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

动态多态,运行期绑定

**虚函数:**使用virtual修饰的成员函数,使其为虚函数。

  1. 非类成员函数不能是虚函数。
  2. 静态函数 static 不能是虚函数,
  3. 构造函数不能是虚函数(析构可以),(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
  4. 内联函数不能是表现多态性质的虚函数
class Shape                     // 形状类
{
public:
    virtual double calcArea()
    {
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 调用圆形类里面的方法
    shape2->calcArea();         // 调用矩形类里面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

class Shape
{
public:
    Shape();                    // 构造函数不能是虚函数
    virtual double calcArea();
    virtual ~Shape();           // 虚析构函数
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    shape1->calcArea();    
    delete shape1;  // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
    shape1 = NULL;
    return 0;
}

纯虚函数

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数有意义的实现而将其声明为纯虚函数,实现留给基类的派生类去做。

virtual int A() = 0;

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类,具体见下文虚继承。

虚函数指针,虚函数表

  • 虚函数指针:在含有虚函数的类的对象中,指向虚函数表,在运行时确定,使对象size + 指针大小(x32 4字节,x64 8字节)
  • 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,在虚表中覆盖原本基类的那个虚函数指针。

虚函数表的实现机制

  • 虚函数表在对象内存的头部,顺序为: 基类的虚函数(若派生类实现了基类的虚函数,则覆盖)+自己的虚函数。
  • 若派生类集成了多个基类,则有虚函数的基类在内存中靠前,没有的靠后。

虚继承

l int A() = 0;`

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类,具体见下文虚继承。

虚函数指针,虚函数表

  • 虚函数指针:在含有虚函数的类的对象中,指向虚函数表,在运行时确定,使对象size + 指针大小(x32 4字节,x64 8字节)
  • 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,在虚表中覆盖原本基类的那个虚函数指针。

虚函数表的实现机制

  • 虚函数表在对象内存的头部,顺序为: 基类的虚函数(若派生类实现了基类的虚函数,则覆盖)+自己的虚函数。
  • 若派生类集成了多个基类,则有虚函数的基类在内存中靠前,没有的靠后。

虚继承

发布了48 篇原创文章 · 获赞 10 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/mhywoniu/article/details/104885231