文章目录
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文件,然后被传递给链接器,
编译和链接是两个阶段
- 编译会检查代码语法和规范的错误,但不检查引用和调用错误
- 链接会检查定义和引用错误,重复的定义和引用会报错,h文件中最好是只定义类型,不具体实现,否则容易出现链接错误。
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++的左值和右值
左右值的特点:
- 左值和右值都是指的表达式,
- 比如 int a = 1 中的 a 是左值,++a 是左值, func() 也可能是左值,而 a+1 是右值, 110 也是一个右值。
- 左值可以放在 = 的左边,右值只能放在 = 的右边,这其中隐含的意思就是左值也能放在 = 的右边,但是右值不能放在 = 的左边。
- 左值可以取地址,代表着内存中某个位置,可以存储数据,右值仅仅是一个值,不能取地址,或者它看起来是一个变量,但它是临时的无法取地址,例如一个函数的非引用的值返回。
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 = #
*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的作用
- 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
- static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
- static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
- 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
- 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。
为什么这段代码不报错?
// 这段代码没有报错,"Moo" 是字符串字面量,其类型在 C++ 里相当于 const char* 有关
virtual const char* speak() {
return "Moo";
}
- 返回类型:函数的返回类型是 const char*,这意味着函数将返回一个指向常量字符的指针。
- 返回值:“Moo” 是一个字符串字面量,它在表达式中会隐式转换为指向其首字符的指针,类型为 const char*。这个指针指向存储 “Moo” 的只读内存区域。