• C/C++要点复习


    前言

    为了准备秋招及春招,现阶段的任务是将所学过的知识进行复习,并串联起来。
    首当其冲的就是编程语言的复习,C++自己平时写的也比较多,但仍有很多语法细节不太记得了,这里浅记一些平常容易忽略的要点

    C++基础入门

    一维数组数组名

    一维数组名称的用途

    1. 可以统计整个数组在内存中的长度(单位:字节B)
    2. 可以获取数组在内存中的首地址

    已知一个数组的数组名,求数组元素的个数,如下:

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    cout << "整个数组所占内存空间为: " << sizeof(arr) << endl;
    cout << "每个元素所占内存空间为: " << sizeof(arr[0]) << endl;
    cout << "数组的元素个数为: " << sizeof(arr) / sizeof(arr[0]) << endl;
    
    • 1
    • 2
    • 3
    • 4

    可以通过数组名获取到数组首地址:

    cout << "数组首地址为: " << (int)arr << endl;
    cout << "数组中第一个元素地址为: " << (int)&arr[0] << endl;
    cout << "数组中第二个元素地址为: " << (int)&arr[1] << endl;
    
    • 1
    • 2
    • 3

    这里首地址即为第一个元素的地址

    指针

    指针所占内存空间

    所有指针类型在32位操作系统下是4个字节

    cout << sizeof(int *) << endl; // 4
    cout << sizeof(char *) << endl; // 4
    cout << sizeof(float *) << endl; // 4
    cout << sizeof(double *) << endl; // 4
    
    • 1
    • 2
    • 3
    • 4

    空指针和野指针

    空指针:指针变量指向内存中编号为0的空间

    int * p = NULL;
    
    • 1

    用途:初始化指针变量
    注意:空指针指向的内存是不可以访问的


    野指针:指针变量指向非法的内存空间

    int * p = (int *)0x1100;
    
    //访问野指针报错 
    cout << *p << endl;
    
    • 1
    • 2
    • 3
    • 4

    两者共同点:都不能访问

    const修饰指针

    const修饰指针有三种情况:

    1. const修饰指针 — 常量指针 (const int * p)
      const修饰的是指针,指针指向可以改,指针指向的值不可以更改

    2. const修饰常量 — 指针常量 (int * const p)
      const修饰的是常量,指针指向不可以改,指针指向的值可以更改

    3. const即修饰指针,又修饰常量 (const int * const p)
      const既修饰指针又修饰常量,两者均不可修改

    指针和数组

    利用指针访问数组中的元素

    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    
    int* p = arr;  //指向数组的指针
    
    cout << "第一个元素: " << arr[0] << endl; // 1
    cout << "指针访问第一个元素: " << *p << endl; // 1
    cout << "指针访问第二个元素: " << *(p+1) << endl; // 2
    
    for (int i = 0; i < 10; i++)
    {
    	//利用指针遍历数组
    	cout << *p << endl;
    	p++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    指针和函数

    利用指针作函数参数,可以修改实参的值

    // 地址传递
    void swap2(int * p1, int *p2) // 声明与定义
    {
    	int temp = *p1;
    	*p1 = *p2;
    	*p2 = temp;
    }
    swap(&a, &b);  // 调用传参
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果是数组名作为函数参数的话,两种写法效果相同,因为数组名就代表了首元素的地址

    void bubbleSort(int * arr, int len)  //int * arr 也可以写为int arr[]
    {
    	for (int i = 0; i < len - 1; i++)
    	{
    		for (int j = 0; j < len - 1 - i; j++)
    		{
    			if (arr[j] > arr[j + 1])
    			{
    				int temp = arr[j];
    				arr[j] = arr[j + 1];
    				arr[j + 1] = temp;
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    结构体

    结构体指针

    通过指针访问结构体中的成员
    利用操作符 -> 可以通过结构体指针访问结构体属性

    student stu = { "张三",18,100};
    student * p = &stu;
    p->score = 80; //指针通过 -> 操作符可以访问成员
    cout << "姓名:" << p->name << " 年龄:" << p->age << " 分数:" << p->score << endl;
    
    • 1
    • 2
    • 3
    • 4

    函数参数

    结构体作函数参数时,如果不想修改主函数中的数据,用值传递,反之用地址传递

    // 声明
    void printStudent2(student *stu);
    // 调用
    printStudent2(&stu);
    
    • 1
    • 2
    • 3
    • 4

    C++核心编程

    内存4大分区

    C++程序在执行时,将内存大方向划分为4个区域

    • 代码区:存放函数体的二进制代码,由操作系统进行管理的。特点共享只读
    • 全局区:存放全局变量、静态变量、常量,该区域的数据在程序结束后由操作系统释放
    • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
    • 堆区:由程序员分配和释放内存(new),若程序员不释放(delete),程序结束时由操作系统回收

    new操作符

    C++中利用new操作符在堆区开辟数据
    利用new创建的数据,会返回该数据对应的类型的指针

    int* a = new int(10); // 创建一个值为10变量
    
    • 1

    开辟数组

    int* arr = new int[10];
    delete[] arr;
    
    • 1
    • 2

    引用&(指针常量)

    作用:给变量起别名,本质上还是原来那个变量

    • 引用必须初始化
    • 引用在初始化后,不可以改变(易错)

    int a = 10;
    int b = 20;
    //int &c; //错误,引用必须初始化
    int& c = a; //一旦初始化后,就不可以更改
    c = b; //这是赋值操作,不是更改引用,a的值也会改变
    
    cout << "a = " << a << endl; // 20
    cout << "b = " << b << endl; // 20
    cout << "c = " << c << endl; // 20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    由于c是a的别名,c在被重新赋值后,a也会随之改变
    这个特点也印证了,引用本质上是一个指针常量,即指针指向的值可以修改,但是指针的指向不允许修改。

    引用作函数参数

    可以代替指针,实现函数内修改实参的操作

    通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单

    void mySwap03(int& a, int& b) {
    	int temp = a;
    	a = b;
    	b = temp;
    }
    mySwap03(a, b);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    函数重载

    函数名可以相同,提高复用性

    函数重载满足条件

    1. 同一个作用域下
    2. 函数名称相同
    3. 函数参数类型、个数、顺序至少要有一个不同

    注:函数返回值不可以作为函数重载条件

    类与对象

    构造/析构函数写法

    类的定义

    class Person {
    public:
    	Person() {
    		cout << "无参构造函数!" << endl;
    		mAge = 0;
    	}
    	Person(int age) {
    		cout << "有参构造函数!" << endl;
    		mAge = age;
    	}
    	Person(const Person& p) {
    		cout << "拷贝构造函数!" << endl;
    		mAge = p.mAge;
    	}
    	//析构函数在释放内存之前调用
    	~Person() {
    		cout << "析构函数!" << endl;
    	}
    public:
    	int mAge;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    利用类创建对象

    Person man1; // 无参构造
    Person man2(100); // 有参构建
    Person newman(man); // 拷贝构造函数
    
    • 1
    • 2
    • 3

    深拷贝与浅拷贝

    浅拷贝:简单的赋值拷贝操作
    深拷贝:在堆区重新申请空间,进行拷贝操作

    如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的重复释放内存空间的问题

    class Person {
    public:
    	Person() {
    		cout << "无参构造函数!" << endl;
    	}
    	
    	Person(int age, int height) {
    		cout << "有参构造函数!" << endl;
    		m_age = age;
    		m_height = new int(height);  // 堆区开辟
    	}
    	
    	//拷贝构造函数  
    	Person(const Person& p) {
    		cout << "拷贝构造函数!" << endl;
    		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
    		m_age = p.m_age;  // 非堆区开辟
    		m_height = new int(*p.m_height);  // 堆区开辟
    
    	}
    
    	//析构函数
    	~Person() {
    		cout << "析构函数!" << endl;
    		if (m_height != NULL)
    		{
    			delete m_height;
    		}
    	}
    public:
    	int m_age;
    	int* m_height; // 堆区开辟
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    列表初始化

    一种简化语法的操作

    class Person {
    public:
    
    	传统方式初始化
    	//Person(int a, int b, int c) {
    	//	m_A = a;
    	//	m_B = b;
    	//	m_C = c;
    	//}
    
    	//初始化列表方式初始化
    	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
    private:
    	int m_A;
    	int m_B;
    	int m_C;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    静态成员变量

    即可以通过对象名访问,也可以通过类名访问(类名::静态成员变量名)
    静态成员变量特点

    1. 在编译阶段分配内存
    2. 类内声明,类外初始化
    3. 所有对象共享同一份数据

    定义与初始化:

    class Person
    {
    public:
    	static int m_A; //静态成员变量
    private:
    	static int m_B; // 如果设为私有,类外也无法直接访问
    };
    int Person::m_A = 10; // 初始化
    int Person::m_B = 10;
    
    Person p1;
    p1.m_A; // 创建对象,利用对象名调用
    Person::m_A; // 直接通过类名调用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    this指针(重要)

    this指针在Java中也存在,不过用法上还是略有区别

    this指针指向被调用的成员函数所属的对象

    this指针的用途:

    • 当形参和成员变量同名时,可用this指针来区分
    • 在类的非静态成员函数中返回对象本身,可使用return *this
    class Person
    {
    public:
    	Person(int age)
    	{
    		//1、当形参和成员变量同名时,可用this指针来区分
    		this->age = age;
    	}
    
    	Person& PersonAddPerson(Person p)
    	{
    		this->age += p.age;
    		//返回对象本身
    		return *this;
    	}
    
    	int age;
    };
    
    // 因返回的是对象本身,所以可以连续调用
    p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    友元

    友元分为三种:

    • 全局函数作友元
    • 类作友元
    • 成员函数作友元

    第一种最简单,直接声明前加上friend关键字,放在类的最前头即可

    friend void goodGay(Building * building);
    
    • 1

    第二种需要考虑到类定义的顺序
    被友元访问的那个类,需要事先声明,顺序如下:

    class B;
    
    class A
    {
    public:
    ...
    private:
    	B *b;
    };
    
    class B
    {
    	friend class A;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    第三种成员函数作友元,与类友元类似,不过只有类中特定的一个成员函数可以访问。总体形式一样,区别在于声明语句

    friend void goodGay::visit();
    
    • 1

    运算符重载

    不能重载的运算符有5种,分别是.->sizeof?:::

    加号

    class Person {
    public:
    	Person() {};
    	Person(int a, int b)
    	{
    		this->m_A = a;
    		this->m_B = b;
    	}
    	//成员函数实现 + 号运算符重载
    	Person operator+(const Person& p) {
    		Person temp;
    		temp.m_A = this->m_A + p.m_A;
    		temp.m_B = this->m_B + p.m_B;
    		return temp;
    	}
    
    public:
    	int m_A;
    	int m_B;
    };
    
    //全局函数运算符重载 可以发生函数重载 
    Person operator+(const Person& p2, int val)  
    {
    	Person temp;
    	temp.m_A = p2.m_A + val;
    	temp.m_B = p2.m_B + val;
    	return temp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    使用方法

    Person p1(10, 10);
    Person p2(20, 20);
    Person p3 = p2 + p1;
    Person p4 = p3 + 10;
    
    • 1
    • 2
    • 3
    • 4

    左移

    只能在全局函数中实现

    //全局函数实现左移重载
    //ostream对象只能有一个
    ostream& operator<<(ostream& out, Person& p) {
    	out << "a:" << p.m_A << " b:" << p.m_B;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    并且要作为友元,放在原类中

    friend ostream& operator<<(ostream& out, Person& p);
    
    • 1

    继承

    基本语法

    class A : public B;

    A 类称为子类 或 派生类
    B 类称为父类 或 基类

    class Base1
    {
    public: 
    	int m_A;
    protected:
    	int m_B;
    private:
    	int m_C;
    };
    
    //公共继承
    class Son1 :public Base1
    {
    public:
    	void func()
    	{
    		m_A; //可访问 public权限
    		m_B; //可访问 protected权限
    		//m_C; //不可访问
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    成员函数变量同名

    1. 子类对象可以直接访问到子类中同名成员
    2. 子类对象加作用域可以访问到父类同名成员
    3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
    s.m_A; // 子类的变量
    s.Base::m_A; // 父类的变量,加类名访问
    s.func(); // 子类的函数
    s.Base::func(); // 父类的函数,加类名访问
    
    • 1
    • 2
    • 3
    • 4

    多态

    父类指针或引用指向子类对象

    • 静态多态: 函数重载和运算符重载属于静态多态,复用函数名——编译阶段确定函数地址
    • 动态多态: 派生类和虚函数实现运行时多态——运行阶段确定函数地址

    父类定义一个虚函数,子类通过重写虚函数实现多态

    // 父类
    virtual void speak() {
    		...
    }
    
    • 1
    • 2
    • 3
    • 4

    计算器

    创建一个计算器父类与加法子类,并重写虚函数

    //多态实现
    //抽象计算器类
    //多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
    class AbstractCalculator
    {
    public :
    
    	virtual int getResult()
    	{
    		return 0;
    	}
    
    	int m_Num1;
    	int m_Num2;
    };
    
    //加法计算器
    class AddCalculator :public AbstractCalculator
    {
    public:
    	int getResult()
    	{
    		return m_Num1 + m_Num2;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    使用方法:

    //创建加法计算器
    AbstractCalculator *abc = new AddCalculator;
    abc->m_Num1 = 10;
    abc->m_Num2 = 10;
    cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
    delete abc;  //用完了记得销毁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    纯虚函数与抽象类

    这个点也是比较陌生的点,平常很少用到不涉及,且和Java有一定程度上的区别

    考虑到父类一般是抽象类,所编写的纯虚函数可能并不存在对应的实现,因此就要把它定义为纯虚函数

    纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

    当类中有了纯虚函数,这个类也称为抽象类

    抽象类特点

    • 无法实例化对象
    • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
    • 类中只要有一个纯虚函数就称为抽象类
    class Base
    {
    public:
    	virtual void func() = 0;
    };
    
    class Son :public Base
    {
    public:
    	void func()
    	{
    		cout << "func调用" << endl;
    	};
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    虚析构与纯虚析构

    存在的问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

    解决方式:将父类中的析构函数改为虚析构或者纯虚析构

    虚析构和纯虚析构共性:

    • 可以解决父类指针释放子类对象(作用),提醒子类必须重写自己的析构函数
    • 都需要有具体的函数实现

    虚析构和纯虚析构区别:

    • 如果是纯虚析构,该类属于抽象类,无法实例化对象
    class Animal {
    public:
    
    	Animal()
    	{
    		cout << "Animal 构造函数调用!" << endl;
    	}
    	virtual void Speak() = 0;
    
    	virtual ~Animal() = 0;
    };
    
    Animal::~Animal()
    {
    	cout << "Animal 纯虚析构函数调用!" << endl;
    }
    
    class Cat : public Animal {
    public:
    	Cat(string name)
    	{
    		cout << "Cat构造函数调用!" << endl;
    		m_Name = new string(name);
    	}
    	void Speak()
    	{
    		cout << *m_Name << "小猫在说话!" << endl;
    	}
    	~Cat()
    	{
    		cout << "Cat析构函数调用!" << endl;
    		if (this->m_Name != NULL) {
    			delete m_Name;
    			m_Name = NULL;
    		}
    	}
    public:
    	string* m_Name;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    未完待续……

    参考资料

    1. 黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难
    2. 《C++ Primer》
  • 相关阅读:
    C++11 新特性 持续记录
    书生大模型实战营-入门第2关-python单词计数
    国产化框架PaddleClas结合Swanlab进行杂草分类
    如何开发一款基于 Vite+Vue3 的在线表格系统(上)
    ICEM使用经验与网格划分错误分析
    Jupyter Notebook 怎么在虚拟环境之间切换
    UE5:如何解决背景图片被拉伸的问题?
    十、K8S之ConfigMap
    机器学习西瓜书学习记录-第三章 线性模型
    强大CSS3可视化代码生成器
  • 原文地址:https://blog.csdn.net/comscience/article/details/125995569