Skip to content
Go back

C++之学习之旅

Updated:  at  11:34 PM

文章目录

c++如何工作

C++ 会为每个cpp文件构建一个obj文件,编译器在构建的时候会处理每个文件。 它默认相信我们的所有声明,所以可能出现只有在执行的时候,才发现声明的函数是否被链接器正确链接 但是,在构建整个项目的时候,也可以发现对应的错误

// main.cpp
#include <iostream>

void Log(const char* message);

void main(){
    Log("hello world!");
    std::cin.get();
}

// log.cpp
#include <iostream>

void Log(const char* message){
    std::cout << message << std::endl;
}

c++编译器如何工作

从文本文件(code)到可执行的二进制文件,有个两个主要动作,一个就是编译,另一个是链接。 编译把cpp文件编译为obj文件,然后被传递给链接器,

编译和链接是两个阶段

c++基础类型

int num = 10;
int num1 = num;
num1 = 20;

std::cout << num << std::endl; // 输出 10
std::cout << num1 << std::endl; // 输出 20

在 C++ 中,对于基本数据类型(如int、char、float等),这种赋值操作是按值进行的。即把num的值10复制一份,然后将这个副本存储到num1所占用的内存空间中。此后,num和num1是两个相互独立的变量,对其中一个变量的修改不会影响到另一个变量

c++的左值和右值

左右值的特点:

  1. 左值和右值都是指的表达式,
    • 比如 int a = 1 中的 a 是左值,++a 是左值, func() 也可能是左值,而 a+1 是右值, 110 也是一个右值。
  2. 左值可以放在 = 的左边,右值只能放在 = 的右边,这其中隐含的意思就是左值也能放在 = 的右边,但是右值不能放在 = 的左边。
  3. 左值可以取地址,代表着内存中某个位置,可以存储数据,右值仅仅是一个值,不能取地址,或者它看起来是一个变量,但它是临时的无法取地址,例如一个函数的非引用的值返回。
int age = 18;
// age是左值,可以被赋值,可以取地址,代表可以存储整数的内存地址

18 = age;
// 编译时会提示错误:error: lvalue required as left operand of assignment
// 18不能代表任何内存地址,不能作为左值

++age++
++(age++)
// 以上两个语句,编译时会提示错误:error: lvalue required as increment operand 

(++age)++
// 可以编译成功

/**后置自增 age++ 的优先级要高于前置自增 ++age 的优先级。**/ 

// 前置自增在原对象上自增,返回的是原对象,而不是自增后的值。
UPInt& UPInt::operator++()
{
 *this += 1;    // 原对象自增
 return *this;  // 返回原对象
}

// 后置自增的一般实现,是先将原对象的数据存储到临时变量中,接着在原对象上实现自增,然后将临时变量以只读的方式返回
const UPInt UPInt::operator++(int)
{
 UPInt oldValue = *this; // 将原对象赋值给临时变量
 ++(*this);              // 原对象自增
 return oldValue;        // 返回临时变量
}

// 既然前置自增返回的是引用,就可以进行赋值
++age = 20;

// 甚至只要函数返回的是左值,也可以进行赋值
int g;
int& func()
{
    return g;
}
func() = 6; // g为6

struct

struct Date{
	int day, month, year;
	Date& dayAdd(int d){
		day += d;
		if (day > 31) {
			month++;
			day = day - 31;
		}
		return *this;
		}
};
返回值类型描述示例
返回*this返回的是Date对象的引用,函数的返回值类型为Date&。
因为返回的是对象的引用,能够进行链式调用。
date.dayAdd(10).print();
返回this返回的是指向Date对象的指针,函数的返回值类型为Date*。
由于返回的是指针,需要使用箭头运算符->来调用成员函数。
date->dayAdd(10)->print();

对象性质无论是返回*this还是this,操作的都是同一个对象。返回this时返回的是指向当前对象的指针,而返回*this时返回的是当前对象的引用,它们都指向或代表同一个对象实例,并非另一个对象。

class

// 一个类中所有东西默认都是私有的
class Creature {
	int HP;
	public:
		int setHP(int num) {
			HP = num;
		}
}

class Log{
public:
	// 枚举
	enum Level
	{
		INFO,
		WARNING,
		ERROR
	};

private:
	// c++11才支持在类中初始化变量
	Level level = INFO;

public:
	void setLevel(Level l)
	{
		level = l;
	}

	void Error(const std::string message)
	{
		if (level >= ERROR)
		{
			std::cout << "[ERROR] " << message << std::endl;
		}
	}

	void Warning(const std::string message)
	{
		if (level >= WARNING)
		{
			std::cout << "[WARNING] " << message << std::endl;
		}
	}

	void Info(const std::string message)
	{
		if (level >= INFO)
		{
			std::cout << "[INFO] " << message << std::endl;
		}
	}
};

int main()
{
	Log log;
	log.setLevel(Log::ERROR);
	log.Error("Hello");
	log.Info("Hello");
	log.Warning("Hello");
}

指针对象和引用对象

引用:使用引用时可以像使用普通对象一样直接访问和操作,不需要额外的运算符。引用就当是给某个已经定义的变量起了个别名。

int num = 10;
int &ref = num;
ref = 20; // 直接使用引用修改所引用对象的值

引用:当引用作为函数参数时,函数内部可以直接修改所引用的对象,但不能让引用引用其他对象。

void modifyByReference(int& ref) {
	ref = 100; // 修改所引用对象的值
	// 不能让ref引用其他对象
	// int anotherNum = 200;
	// ref = anotherNum; // 这只是修改了ref引用对象的值,而不是让ref引用其他对象
}

指针:使用指针时需要使用解引用运算符 * 来访问它所指向的对象,或者使用箭头运算符 -> 来访问对象的成员。

int main(){
  int num = 10;

  int *ptr = &num;
  *ptr = 20; // 使用解引用运算符修改所指向对象的值
  
  struct Point
  {
    int x;
    int y;
  };
  
  Point p = {1, 2};
  Point *ptrPoint = &p;
  ptrPoint->x = 3; // 使用箭头运算符访问对象的成员
}

指针:当指针作为函数参数时,函数内部可以修改指针所指向的对象,也可以修改指针本身的值(即让指针指向其他对象)。

void modifyByPointer(int* ptr) {
	*ptr = 100; // 修改指针所指向的对象的值
	// 可以修改指针本身的值,让它指向其他对象
	int anotherNum = 200;
	ptr = &anotherNum; // &取出来的就是指针
}

类的构造函数

class Entity {
private:
    int age;
    std::string name;
public:
    Entity() : name("UNKNOWN") {}
    Entity(const std::string& name) :name(name), age(-1) {}
    Entity(int age) :name("UNKNOWN"), age(age) {}
};

int main(){
    {
			std::unique_ptr<Entity> entity(new Entity());
			std::unique_ptr<Entity> entity = std::make_unique<Entity>();
      entity->printInfo();
    }

    std::cin.get();
}

c++定义类的数组

c++ 有某个类 Employee; 这样写会报错: Employee e1[422]; 但这样写就不会: Employee *e1[422];


class Employee
{
	string name;
		
public:
	// 为类添加默认构造函数后,Employee e1[422]也不会报错
	Employee() {}
}
// 直接创建对象数组,需要在栈上分配对象内存并调用构造函数初始化,可能因栈空间不足或缺少默认构造函数而报错。
Employee e1[422];  // Error: 可能因栈空间不足或缺少默认构造函数而报错。

// 创建的是指针数组,只需要分配指针的内存空间,不会调用构造函数,不会出现上述问题,但需要手动管理对象的内存分配和释放。
Employee *e1[422]; 

类实例化两种方式,new与不new

// 1. 不用new
A a;
int b[10];

// 2. 用new
A * a = new a();
int* b = new int[5];
delete a;
delete[] b;	// 会调用数组内每个对象的析构函数,不加[]就只会调用第一个对象的析构函数

// 前者在堆栈中分配内存,后者为动态内存分配,在一般应用中是没有什么区别的,但动态内存分配会使对象的可控性增强。
// 不加new在堆栈中分配内存
// 大程序用new,小程序直接申请
// 只是把对象分配在堆栈内存中
// new必须delete删除,不用new系统会自动回收内存

虚函数

虚函数: 虚函数可以让指针在自动转换的时候,仍保证调用子类实现的同名函数

using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::vector;

class Employee
{
	string name;

public:
	// 为类添加默认构造函数后,Employee e1[422]也不会报错
	Employee(){};
	Employee(string n);
	
	// virtual标明是虚函数,虚函数表就是一个表,它包含基类中所有虚函数的映射,
	// 这样我们可以在它运行时将它们映射到正确的覆写(override)函数
	virtual void print();
};

class Manager : public Employee
{
	int level;

public:
	Manager(string n, int l = 1);
	void print();
};

class CEO : public Manager
{
	int stock;

public:
	CEO(string n, int l = 1, int s = 100);
	void print();
};

Employee::Employee(string n) : name(n) {}
Manager::Manager(string n, int l) : Employee(n), level(l) {}
CEO::CEO(string n, int l, int s) : Manager(n, l), stock(s) {}

void Employee::print()
{
	cout << name << endl;
}

void Manager::print()
{
	cout << level << "\t";
	Employee::print();
}

void CEO::print()
{
	cout << stock << "\t";
	Manager::print();
}

int main()
{
	Employee e1[422]; // 需要手动实现Employee的空构造函数,否则会报错
	Employee *p;
	Manager m("Zhang", 1);
	Employee e("Li");
	CEO ceo("Wang");

	// 以下的print函数根据实际指向对象所拥有的方法来执行
	p = &e;
	p->print();	// Li
	p = &m;
	p->print();	// 1       Zhang

	p = &ceo;
	p->print(); // 100     1       Wang
}

虚函数相关的运行时成本:

首先,我们需要额外的内存来存储虚函数表,这样我们才可以分配到正确的函数,包括基类中要有一个成员指针指向虚函数表。 其次,我们调用虚函数时,我们需要遍历这个表来确定映射到哪个函数,这是额外的性能损失。 继承了父类的虚函数,子类就必须要实现,否则虚函数表就会报错

纯虚函数

纯虚函数是在基类中声明的虚函数,它没有具体的实现,而是在函数声明后加上 = 0。纯虚函数的作用是为派生类提供一个统一的接口,派生类必须实现这个纯虚函数。

class Base{
public:
	// 纯虚函数
	virtual const char* speak() = 0;
};

头文件重复问题

// 解决方法1:
#pragma once	// 预处理指令,只编译一次
void Log(const char* message);
void InitLog();

struct player {};

// 解决方法2:
+#ifdef _LOG_H	// 根据预定义的变量进行判断
+#define _LOG_H

void Log(const char* message);
void InitLog();

struct player {};

+#endif

头文件的引用方法

#include <stdlib.h>
#include <iostream> // 尖括号来告诉编译器搜索包含路径文件夹
#include "Log.h"		// 引号则通常用于包含相对于当前文件的文件

C 标准库通常会有.h 拓展但 C++ 文件没有,这是一种区分 C 标准库和 C++ 标准库的方法。

C/C++中的指针*是靠近数据类型还是靠近数据变量书写

// 如下语句很容易理解为p和q都是int类型的指针变量,但其实只有p是int类型的指针变量,q只是普通的int类型的变量而已
int* p, q;

// 建议指针*靠近变量书写,同理C++中的引用&也靠近变量书写.
int *p, q;
int &r = q;

范围解析运算符 ::

/** 将 :: 用于类和命名空间 **/ 
namespace NamespaceA{
    int x;
    class ClassA {
    public:
        int x;
    };
}

int main() {
    // A namespace name used to disambiguate
    NamespaceA::x = 1;
    // A class name used to disambiguate
    NamespaceA::ClassA a1;
    a1.x = 2;
}

/** 没有范围限定符的范围解析运算符表示全局命名空间。 **/ 
namespace NamespaceA{
    int x;
}

int x;

int main() {
    int x;
    // the x in main()
    x = 0;

    // The x in the global namespace
    ::x = 1;

    // The x in the A namespace
    NamespaceA::x = 2;
}

/** 将 :: 用于静态成员 **/
class ClassG {
public:
	static int get_x() { return x;}
	static int x;
};

int ClassG::x = 6;

int main() {
	int gx1 = ClassG::x;
	int gx2 = ClassG::get_x();
}


/** 将 :: 用于范围枚举 **/
enum class EnumA{
	First,
	Second,
	Third
};

int main() {
	EnumA enum_value = EnumA::First;
}

const 和 *

不同编程风格的人使用 int const*,但要知道它和 const int* 是完全一样的,但如果你搞成 int* const,那就完成不同了

int const* a = 2;	// a和b是完全一样的,都代表一个常量指针
const int* b = 2;	// *a和*b所指的内容都不能变,但是a和b可以改变

int* const c = 2; // *c所指的内容可以改变,但是c这个指针常量不能改变

const int* const a = new int; // 这意味着我不能改变指针指向的内容,也不能改变这个指针本身

const只修饰紧跟在它后面的内容,除了类方法

class Entity {
private:
	int m_X, m_Y;
	mutable std::string m_Z;

public:
		// 这里的const表示这个方法不能修改类的成员变量
    std::string getX() const { 
			m_Z = "test mutable";
			return m_Z;
		}
};


// 这里的const则表示传入的是一个常量
void printEntity(const Entity& e) {
		// 如果去掉getX的const就会报错:e has type "const Entity", but function "Entity::getX" is not marked const
    std::cout << e.getX() << std::endl;
}

int main()
{
	Entity e;
	printEntity(e);
	const Entity obj(20); // obj 是常量对象,不能调用非 const 成员函数
}

override

override 关键字用于派生类中,表明该函数是对基类中虚函数的重写。它可以帮助编译器检查是否正确重写了基类的虚函数,如果派生类中的函数签名与基类中的虚函数不匹配,编译器会报错。

void func() override {
	std::cout << "Derived::func()" << std::endl;
}

使用 override 关键字明确表示它是对基类 中 func 虚函数的重写。

final 关键字

// 修饰类:当 final 用于类名之后时,表明该类不能被继承
class FinalClass final {
	// 修饰虚函数:当 final 用于虚函数的声明之后时,表明该虚函数不能在派生类中被重写。
	virtual void func() final {
		// 函数实现 
	}
}

noexcept 关键字

noexcept 关键字用于表明函数不会抛出异常。

void func() noexcept {
	std::cout << "This function does not throw exceptions." << std::endl;
}

lambda表达式

auto f = [&x]() {
		std::cout << x << std::endl;
};

// 或者这样直接传值:
auto f = [x]() {
		std::cout << x << std::endl;
};

// 或者通过 = 号对所有变量进行传值传递:
auto f = [=]() {
		std::cout << x << std::endl;
};

// 或者通过引用符号 & 对所有的变量进行引用传递:
auto f = [&]() {
		std::cout << x << std::endl;
};

// mutable使外部的入参copy可以被修改
auto f = [=]() mutable {
		x++;
		std::cout << x << std::endl;
};

数组

#include <iostream>

int main(){
    int example[5];			// 数组实际上就是个指针
	  int* ptr = example;	
    
    for (int i = 0; i < 5; ++i) {
			// 下面两行代码是等价的
			example[i] = 2;
			*(ptr + i) = 2;	// 在指针加上像 2 这样的值计算实际要加的字节数(偏移)取决于类型。这里我们是整数 int 指针,将会增加 2 * 4 的偏移(4 时每个整型 int 的大小)。
    }

		char* name = "YOUSAZOE";
   	char name1[8] = {'Y','O','U','S','A','Z','O','E'};

    std::cin.get();
}

// C++11 的数组,你可以这样做:

std::array<int, 5> another;

for (int i = 0; i < another.size(); ++i) {
  	another[i] = 2;
}

C/C++ 中static的作用

  1. 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  2. static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  3. static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  4. 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
  5. 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

为什么这段代码不报错?

// 这段代码没有报错,"Moo" 是字符串字面量,其类型在 C++ 里相当于 const char* 有关
virtual const char* speak() { 
	return "Moo"; 
}


Previous Post
Nodejs之process
Next Post
Nodejs之学习笔记