【超全】C++速查手册:面向对象与继承多态

C++速查手册

C++ 类和对象

面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

struct Student
{
    char _name[20]; 
    char _gender[3]; 
    int _age;
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name); strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
}

上面结构体的定义,在C++中更喜欢用class来代替

类的定义

class className
{
		// 类体:由成员函数和成员变量组成
};		// 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。

类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方式:

  1. 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成**内联函数**处理。

    // Person.h
    class Person
    {
    public:
        // 类成员函数
    	void ShowInfo()
    	{
    		cout << _Name << "-" << _Sex << "-" << _Age << endl;
    	}
    public:
        // 类属性
    	char* _Name;
    	char* _Sex;
    	int _Age;
    };
    
  2. 声明放在.h文件中,类的定义放在.cpp文件中。

    // Person.h
    class Person
    {
    public:
        // 类成员函数声明
    	void ShowInfo();		// 头文件声明
    public:
        // 类属性
    	char* _Name;
    	char* _Sex;
    	int _Age;
    };
    
    // Person.cpp
    #include "Person.h"		// 源文件定义
    void Person::ShowInfo()		// 类成员函数的定义
    {
        cout << _Name << "-" << _Sex << "-" << _Age << endl;
    }
    

一般情况下,编译器更期望采用第二种方式。

类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

public
private
protected
访问限定符
共有
私有
保护

访问限定符说明

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. class的默认访问权限为privatestruct为public(因为struct要兼容C)。
  4. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

封装

【面试题】 面向对象的三大特性:封装、继承、多态。但在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。

类的作用域

类定义了一个新的作用域,类的所有成员都会在类的作用域中。在类体外定义成员,需要使用 :: 域限定操作符指明成员属于哪个类域。

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};

// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " "_gender << " " << _age << endl;
}

类的实例化

用类类型创建对象的过程,称为类的实例化

  1. 只是一个模型一样的东西,描述了一个对象的各类属性。限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

类对象的大小计算

对象里面,只有成员变量。

方法:同结构体计算方式,计算类或者类对象大小,只看成员变量,考虑内存对齐。

// 类中既有成员变量,又有成员函数
class A1 
{
public:
	void f1() {}
private:
	int _a;
};

// 类中仅有成员函数
class A2 
{
public:
	void f2() {}
};

// 空类——类中什么都没有
class A3
{};

int main()
{
	cout << "A1 sizeof: " << sizeof(A1) << endl;		// 4
	cout << "A2 sizeof: " << sizeof(A2) << endl;		// 1
	cout << "A3 sizeof: " << sizeof(A3) << endl;		// 1
	return 0;
}

一个类的大小,实际就是该类中**”成员变量”之和**,同时也要进行内存对齐(C++内存对齐规则跟c结构体一致)。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类

结构体内存对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

    VS中默认的对齐数为8

  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

  5. 静态成员变量不会参与内存对齐,因为它是放在静态区的变量。

类对象存储位置

  1. 成员变量存储在对象内,对象又存储在全局变量区**(静态区)**内;
  2. 成员函数存储在代码区内,需要时才会需要call对应的函数。

C++ this指针

C++ this指针:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

image-20220715003215007

this指针的特性:

  1. this指针的类型:类的类型* const(前置const修饰的是指针指向的值无法被修改,后置const修饰的指针是指针无法被修改
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
//隐含传递this指针演示:

// 类中的成员函数
void Display()
{
	cout << _year << endl;
}

// 实际的类中成员函数
void Display(Date* this)
{
    cout << this->_year << endl;
}
  1. this指针一般存储在哪?

    答:一般来说是存储在“栈区”上(因为它是作为函数形参传入类中,而由函数栈帧所学的可以得到的是,参数一般存储在“栈”上)。部分编译器会存储在寄存器ecx/rcx内,例如VS;

  2. 观察下面的代码,查看是否能编译通过。

    // 1.下面程序能编译通过吗?
    
    class A
    {
    public:
    	void Show()
    	{
    		cout << "Show()" << endl;
    	}
    
    private:
    	int _a;
    };
    
    int main()
    {
    	A* p = nullptr;
    	p->Show();
    	return 0;
    }
    
    // 2.下面程序能编译通过吗?如果会崩溃,在哪里崩溃?
    class A
    {
    public:
    	void PrintA()
    	{
    		cout << _a << endl;
    	}
    private:
    	int _a;
    };
    
    int main()
    {
    	A* p = nullptr;
    	p->PrintA();
    	return 0;
    }
    

    答案:代码段1会通过,且能运行;代码段2会通过,但会崩溃。

    解析:

    • p虽然是空指针,但是p调用成员函数不会编译报错,因为空指针不是语法错误,编译器检查不出来。
    • p虽然是空指针,但是p调用成员函数不会出现空指针访问。因为成员函数没有存在对象里面。
    • 这里会把p作为实参传递给隐藏this指针。
    • 因为会把p作为实参传递给隐藏this指针,代码段1中并没有对空指针this进行解引用,而代码段2中对空指针this进行了解引用访问了成员变量,所以发生了运行崩溃。

static成员

概念:

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰成员函数,称之为静态成员函数静态的成员变量一定要在类外进行初始化。

特性:

  1. 静态成员为所有类对象所共享,不属于某个具体的实例;

  2. 静态成员变量必须在类外定义,定义时不添加static关键字,属于整个类、所有对象,生命周期是整个程序运行周期;

    class Solution
    {
    private:
        static int _s;
    }
    
    int Solution::_s = 0;
    
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

内部类

概念:

如果一个类定义在另一个类的内部,这个类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性:

  1. 内部类和在全局定义的类是基本一致的,只是它受到外部类的类域限制、
  2. 内部类可以定义在外部类的public、protected、private都是可以的,且会受到同样的权限保护。
  3. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  4. sizeof(外部类)=外部类,和内部类没有任何关系。

简单来说,内外部类的关系就是,外部类对于内部类的单相思,外部类包含于内部类,内部类可以访问外部类的私有成员,外部不可以访问内部私有。简称称:“TG”(Bu)。除非内部类给外部类添加友元(转正),否则外部类只能看着。

类的6个默认成员函数

class A {};

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

构造函数
析构函数
拷贝构造
赋值重载
取地址重载
6个默认成员函数
初始化和清理
主要完成初始化工作
主要完成清理工作
拷贝复制
拷贝构造是使用同类对象初始化创建对象
主要是把一个对象赋值给另一个对象
取地址重载
主要是普通对象和const对象取地址,这两个很少会自己实现
成员函数 作用
构造函数 主要完成初始化工作
析构函数 主要完成清理工作
拷贝构造 拷贝构造是使用同类对象初始化创建对象
赋值重载 主要是把一个对象赋值给另一个对象
取地址重载 主要是普通对象const对象取地址,这两个很少会自己实现
构造函数

构造函数:构造函数一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。

特性:

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名与类名相同。

    class A
    {
    	A() {}
    }
    
  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数(不用参数就可以调用的构造函数),并且默认构造函数只能有一个。(语法上他们两个可以同时存在,但是有对象定义去调用时会出错)

    注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认成员函数。

构造函数的理解
构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
初始化成员变量列表

在类实例化时,成员变量仅仅是声明并没有定义,如果我们需要在声明时进行定义,C++引入了初始化成员变量列表。

初始化列表:初始化列表是成员变量定义的地方,它以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次);

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

    • 引用成员变量;
    • const成员变量;
    • 自定义类型成员(如果该类没有默认构造函数,会出现编译失败) 。
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化

    • 一般建议在初始化成员列表内进行自定义类型变量的定义,这样会更高效一些。

      因为即使你不在初始化列表内定义自定义类型成员变量,编译器也会在初始化列表内自动调用一次自定义类型成员变量的默认构造函数进行一次初始化。如果后续在构造函数内声明自定义成员变量的值,只是再调用自定义类型内的赋值运算符进行内容赋值。

  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

    建议:一个类,尽量声明的顺序和初始化列表出现的顺序保持一致,就不容易出问题。

    class A
    {
    public:
    	A(int a)
    		:_a1(a)
    		, _a2(_a1)
    	{}
    	
    	void Print()
    	{
    		cout << _a1 << " " << _a2 << endl;
    	}
    private:
    	int _a2;
    	int _a1;
    };
    
    int main()
    {
    	A aa(1);
    	aa.Print();
    }
    
    //上面的代码进行编译运行时,程序会输出什么结果?
    // A. 输出1 1
    // B. 程序崩溃
    // C. 编译不通过
    // D. 输出1 随机值
    
    // 答案:D
    // 解析:成员变量在类 [声明] 次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
    // 上题中,首先声明的是a2,因为a2初始化定义为a1,但a1还没有定义,所以a2是随机值,而后就到a1的声明和定义,取得参数a的值。
    
  5. 初始化列表的选择

    • 内置类型的成员,在函数体和在初始化列表初始化都可以;
    • 自定义类型的成员,建议在初始化列表初始化,这样更高效。
explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。

class Date
{
public:
	Date(int year)
		:_year(year)
	{}
    
    // 如果你不希望编译器像下面的隐式类型转换
    // 你需要在构造函数之前添加一个explicit关键字,这样编译器就不会在该类型内进行隐式类型转换
	explicit Date(int year)
		:_year(year)
	{}
private:
	int _year;
	int _month :
	int _day;
};

void TestDate()
{
    // 虽然它们俩都是直接构造,但是它们各自在运行过程中经历的是不一样的
	Date d1(2021);
    
    //用一个整形变量给日期类型对象赋值
	d1 = 2022; 		// 因为它有构造函数的参数都是int类型,编译器会自动识别他们是相似类型,所以支持隐式类型转换
	// 实际编译器背后会用2022构造一个临时对象,最后用临时对象给d1对象进行拷贝构造赋值
    // 但是现代的C++编译器在连续的一个过程中,多个构造会被优化合二为一
    // 所以这里被优化为直接就是一次构造
    
}

析构函数

析构函数:析构函数是特殊的成员函数。与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

特性:

  1. 析构函数名是在类名前加上字符 ~

    class A
    {
    	~A() {}
    }
    
  2. 无参数无返回值。

  3. 一个类有且只有一个析构函数。若未显式定义,编译器会自动生成默认的析构函数。

  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

  5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数

拷贝构造函数

构造函数:拷贝构造函数也是特殊的成员函数。只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

// 拷贝构造实例
void f(Date d)
{}

int main()
{
    Date d1(2022, 1, 15);
    f(d1);					// 调用一份d1的构造函数作为参数传到f内
    return 0;
}

特征:

  1. 拷贝构造函数是构造函数的一个重载形式

  2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用

    **原因:**因为在函数传值时,形参是实参的一份临时拷贝。在有两个类A,B,现在需要把A(B)将B作为值形参传入A中进行传参。因为是值拷贝,在生成形参时系统为了把值复制进去,就会申请一份的空间给变量B,大小是变量A的大小。同时再调用变量A的拷贝构造函数把值给到B。调用函数之后再重新申请空间,当调用自身的拷贝构造函数的时候又因为是值传递,就导致了无限递归。

  3. 若未显示定义系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

    • 默认生成拷贝构造:

      内置类型成员:会完成按字节序的拷贝。(浅拷贝)

      自定义类型成员:会调用他的拷贝构造。(深拷贝)

  4. 拷贝构造我们不写生成的默认拷贝构造函数对于内置类型和自定义类型都会拷贝处理。但是处理细节是不一样的,这个跟构造和析构是不一样的。

  5. 一次调用里面,如果连续构造函数,会被编译器优化——合二为一。(优化的时机:传参或返回过程中,存在连续的构造、拷贝构造、就会被优化)。

    class A
    {
    public:
    	A(const A&)
    	{
    		cout << "A(const A&)" << endl;
    	}
    }
    
    A func(A x)
    {
    	return x;
    }
    
    int main()
    {
    	A a;
    	A b = func(a);		
        // 在这里,如果按照默认和正常逻辑来说,是要进行3次的拷贝构造,进去一次,出来return一次,赋值一次,但编译器会优化成2次;
        // (这里还有一个前提是,=号未重载,否则会去调用重载函数)
        
    	func(d());			// 同理,按理来说,这里应该会进行2次,而因为是使用了匿名构造,被优化成1次;
    	// 虽然C++标准里并没有明确说明需要编译器进行优化,但是我们现在使用的编译器基本上都进行了优化;
    	return 0;
    }
    
    // 试一试计算下面的代码:不考虑编译器优化的问题,这里一共调用几次拷贝构造函数?
    class Widget
    {
    public:
    	Widget(const Widget &)
        {
            cout << "Widget(const Widget &)" << endl;
        }
    }
    
    Widget f(Widget u)
    {
    	Widget v(u);
    	Widget w = v;
    	return w;
    }
    
    int main()
    {
    	Widget x;
    	Widget y = f(f(x));
        
        return 0;
    }
    
    // 答案:9次; 	(优化的是:7次)
    // 解析:x->f(x)=u, u->v(u)=v, v->w=w, w->f(x)=f(x), f(x)->u=u, u->v=v, v->w=w, w->f(x)=f(f(x)), f(f(x))->y
    // 优化后:x->f(x)=u, u->v(u)=v, v->w=w, w->u=u(这里编译器会直接把w作为下一次调用的参数),
    //  	  u->v(u)=v, v->w=w, w->f(f(x))=y
    

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator 后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表) 。

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义。
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参。
  • (. 、:: 、sizeof 、?: 、.)* 注意以上5个运算符不能重载。这个经常在笔试选择题中出现
  • 如果运算符重载在全局里面,如果是双操作数的操作符重载第一个参数是左操作数,第二个参数是右操作数
  • 如果运算符重载在类里面,那么它的默认参数是第一个,隐含this指针。当重载流运算符在类里时,会出现 cls << cout,这时候一般建议在全局内重载。
赋值运算符重载

赋值运算符主要有四点:

  1. 参数类型
  2. 返回值(返回*this)
  3. 检测是否自己给自己赋值
  4. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

前置++和后置++的重载

通常写法来说,我们定义一个前后置++的运算符方法是 参数类型 operator++()。因为++的设计原因我们不需要在参数列表内设置任何参数,但如果要同时写下两个重载时就会出现了一个前后置++函数写法是一样的。这个时候就可以引申出前面学的函数重载知识了,C++为了区分后置++,我们可以在参数列表内添加int来进行占位,编译器在处理时会自动将参数列表有int的区分为后置++,而传入的参数自动为0,同理,我们只能设置int类型的参数在后置++重载函数列表内(否则会参数报错)。

// ++T
T& T::operator++()		// 实际传参: operator(T& const this)
{}

// T++
T& T::operator++(int)	// 实际传参: operator(T& const this, 0)

const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

前置const修饰的是指针指向的值无法被修改,后置const修饰的指针是指针无法被修改

// 编译器对const成员函数的处理

// 表面编码
class Date
{
public:
    void DisPlay() const
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}

// 编译器处理
class Date
{
public:
    void DisPlay(const Date* const this)	//  const Date* const this  两层const保护this指针不被改变
    {
        cout << this->_year << "_" << this->_month << "_" << this->_day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}

小Tips:

在实际编程中,成员函数加const是好的,建议能加const都加上。这样普通对象和const对象都可以调用了。但是如果成员函数要修改成员变量是不能加的。

C++ inline内联函数

在之前的学习中我们了解到,程序在调用函数时都会形成对应到的函数栈帧。如果一个函数内容很多例如仅仅只需要完成A+B的操作,只是为了一个封装而花费时间进行建立栈帧,累加起来就会浪费太多时间建立栈帧。C里面有宏替换但不易调试,而C++引入了内联函数,为开发者提供了更好的选择。

inline是C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。

当函数在返回值前加入inline关键字时,编译器会将函数原地展开运行并将结果返回,减去建立栈帧的过程,这就是inline关键字的作用。

需要特别注意的是,标注内联的函数在编译器不一定会处理为内联inline只是给编译器在编译时提供一个优化建议,编译器还需要根据函数代码量、复杂度作为参考,考虑是否将其展开作为内联。

C++11 的成员初始化新玩法

静态成员变量声明时初始化

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值

因为这里是声明,不能初始化。初始化严格来说是给某个空间初始化,a变量是声明,而不是作为某个空间能进行初始化。

class B
{
public:
	B(int b = 0)
		:_b(b)
	{}
	int _b;
};

class A
{
public:
	void Print()
	{
		cout << a << endl;
		cout << b._b << endl;
		cout << p << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
    // 如果在初始化列表阶段没有对成员变量初始化,那么它就会使用缺省值进行初始化。
	int a = 10;
	B b = 20; 
    int* p = (int*)malloc(4);
    int array[10] = {1, 2, 3, 4, 5};
    
	static int n; // 静态成员变量不能给缺省值
    // 因为缺省值是给构造函数进行参考使用的,构造函数里不处理静态成员
};

int A::n = 10;

int main()
{
	A a;
	a.Print();
	return 0;
}

C++友元

友元分为:友元函数友元类

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

友元函数可以直接访问类私有成员,它是定义在类外部普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

说明:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数,它属于全局函数;
  2. 友元函数不能用const修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数;
  5. 友元函数的调用与普通函数的调用和原理相同。

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。

    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time

    类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

  • 友元关系不能传递

    如果B是A的友元,C是B的友元,则不能说明C时A的友元。

友元实例(输入输出流重载)

现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1970, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}

int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

C++ 继承

简介: C++ 继承是一种面向对象编程特性,允许一个类(子类)从另一个类(基类)继承属性和方法。

详细介绍:
C++ 继承具有以下特点:

  • 子类从基类继承数据成员和成员函数
  • 可以实现代码的复用和模块化,降低维护成本
  • 支持多层继承和多重继承,提高代码的可扩展性

语法用途模板:

  • 基类: 定义基本属性和方法,用于被子类继承
  • 子类: 从基类继承属性和方法,可以添加或覆盖基类的成员
  • 访问控制: 使用 public, protected, private 关键字限制类成员的继承权限

应用场景: C++ 继承适用于各种类型的项目,用于实现面向对象编程的封装、继承和多态特性。

语法详细代码:

#include 

// 基类定义
class Base
{
public:
    int x;

    void display() 
    {
        std::cout << "Base class x = " << x << std::endl;
    }
};

// 子类定义
class Derived : public Base
{
public:
    int y;

    void display()
    {
        std::cout << "Derived class y = " << y << std::endl;
    }
};

int main()
{
    // 基类对象
    Base baseObj;
    baseObj.x = 5;
    baseObj.display();

    // 子类对象
    Derived derivedObj;
    derivedObj.x = 10;
    derivedObj.y = 20;
    derivedObj.display();

    return 0;
}

C++ 多态

简介: C++ 多态是一种面向对象编程特性,允许使用基类指针或引用调用派生类的成员函数。

详细介绍: C++ 多态具有以下特点:

  • 使用虚函数实现多态,动态地调用成员函数。
  • 可以提高代码的可扩展性和可维护性。
  • 支持运行时多态和编译时多态。

语法用途模板:

  • 虚函数: 使用 virtual 关键字声明基类的成员函数,以便在派生类中重写。
  • 派生类: 重写基类的虚函数,实现多态。
  • 基类指针或引用: 用于指向派生类对象,调用派生类的成员函数。

应用场景: C++ 多态适用于各种类型的项目,用于实现面向对象编程的封装、继承和多态特性。

语法详细代码:


#include 

// 基类定义
class Base
{
public:
    virtual void display() 
    {
        std::cout << "Base class display" << std::endl;
    }
};

// 派生类定义
class Derived: public Base
{
public:
    void display()
    {
        std::cout << "Derived class display" << std::endl;
    }
};

int main() 
{
    // 基类指针
    Base* basePtr;
    // 基类对象
    Base baseObj;
    basePtr = &baseObj;
    basePtr->display();

    // 派生类对象
    Derived derivedObj;
    basePtr = &derivedObj;
    basePtr->display();

    return 0;
}

C++ 文件和流

简介: C++ 文件和流用于处理文件和数据流,提供了一种在程序中读写文件和数据流的方法。

详细介绍: C++ 文件和流具有以下特点:

  • 使用 fstream、ifstream 和 ofstream 类进行文件操作
  • 提供了读、写、追加等文件操作模式
  • 支持文本文件和二进制文件处理

应用场景: C++ 文件和流适用于需要对文件和数据流进行操作的项目。

语法详细代码:

#include 
#include 
#include 

int main() 
{
    // 创建一个文件输出流对象(用于写入文件)
    std::ofstream outFile("example.txt", std::ios::out);

    // 检查文件是否成功打开
    if (!outFile.is_open())
    {
        std::cerr << "Failed to open the file." << std::endl;
        return 1;
    }

    // 将数据写入文件
    outFile << "This is an example text." << std::endl;

    // 关闭文件
    outFile.close();

    // 创建一个文件输入流对象(用于读取文件)
    std::ifstream inFile("example.txt", std::ios::in);

    // 检查文件是否成功打开
    if (!inFile.is_open())
    {
        std::cerr << "Failed to open the file." << std::endl;
        return 1;
    }

    // 从文件中读取数据
    std::string line;
    while (std::getline(inFile, line)) 
    {
        std::cout << line << std::endl;
    }

    // 关闭文件
    inFile.close();

    return 0;
}

C++ 异常处理

简介: C++ 异常处理是一种错误处理机制,用于在程序运行过程中捕获和处理异常。

详细介绍: C++ 异常处理具有以下特点:

  • 使用 try、catch 和 throw 语句进行异常处理
  • 可以自定义异常类,提高程序的可维护性
  • 支持异常的层级传递和嵌套处理

应用场景: C++ 异常处理适用于需要处理运行时错误的项目,提高程序的稳定性和健壮性。

语法详细代码:

#include 
#include 

// 自定义异常类
class CustomException : public std::exception 
{
public:
    const char* what() const noexcept 
    {
        return "This is a custom exception.";
    }
};

void throwException(bool flag) 
{
    if (flag)
    {
        throw CustomException();
    } 
    else 
    {
        throw std::runtime_error("This is a runtime_error.");
    }
}

int main() 
{
    try 
    {
        throwException(true);
    } 
    catch (const CustomException& e) 
    {
        std::cerr << "Caught CustomException: " << e.what() << std::endl;
    } 
    catch (const std::runtime_error& e) 
    {
        std::cerr << "Caught runtime_error: " << e.what() << std::endl;
    } 
    catch (...)
    {
        std::cerr << "Caught an unknown exception." << std::endl;
    }

    return 0;
}

C++ 预处理器

简介: C++ 预处理器是一种编译前的文本处理工具,用于处理源代码中的预处理指令。

详细介绍: C++ 预处理器具有以下特点:

  • 使用预处理指令(以 # 开头)进行条件编译、宏定义和文件包含等操作
  • 预处理指令在编译前处理,不影响运行时性能
  • 提高代码的可读性和可维护性

应用场景: C++ 预处理器适用于需要控制编译过程、定义宏和包含文件的项目。

语法详细代码:

#include 

// 宏定义
#define PI 3.14159
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

// 条件编译
#ifdef PI
    #define AREA(r) (PI * (r) * (r))
#else
    #define AREA(r) (3.14 * (r) * (r))
#endif

int main() 
{
    // 使用宏
    std::cout << "PI = " << PI << std::endl;
    std::cout << "Min(3, 4) = " << MIN(3, 4) << std::endl;
    std::cout << "Area(5) = " << AREA(5) << std::endl;

    return 0;
}

|

你可能感兴趣的:(c++,面试,c语言,开发语言)