CPP是如何工作的

编译器将所有代码转化成机器代码,这一过程叫【编译】,【编译】是可配置的。可以选择模式与目标平台。Release模式比Debug模式快。

编译的时候,所有cpp文件会被编译,而头文件不会。每个cpp文件里面包含进来的文件一起被编译了。

每个cpp文件都被编译成了一个个目标文件object file,他们以.obj为后缀。


编译完成之后,要把一个个obj 合并成一个可执行.exe文件,这就需要【链接】,link

链接也可以实现在一个文件声明declaration,在另一个文件定义defination的操作。比如在Log.cpp定义Log()函数,在Main.cpp声明Log()函数,之后Main.cpp就可自由调用Log()函数了

//Log.cpp
#include <iostream>

void log(const char* msg)
{
    std::cout<<msg<<std::endl;
}
//Main.cpp
void log(const char* msg);
//void log(const char*)省去参数名也可以

int main()
{
    log("hello world");
}

函数的声明,起始就是没有函数体的函数,直接在函数名结尾加个分号就结束了。

编译

cpp中,文件没有意义。文件只是喂给编译器的文本载体。

预处理操作

#include的实际操作

把#include的文件或库里的文本,丝毫不差的粘贴到#include所在行

<>与""的区别

  • <>是官方指定路径
  • ""是自定义路径,一般来说凡是能用<>引用的,""也可以引用

#define A B

把文本内容中所有的文本A替换为文本B

#if X #endif

如果X=0,那么从#if到#endif这一整段代码都不会被编译;反之,会被编译。

#pragma once

只复制粘贴一次。用在头文件开头,确保一个cpp文件只能引用该头文件一次,以防止重复定义等错误发生。你也许会好奇,同一个文件我怎么会引用两次捏,我有那么傻吗?哈哈,年轻人,我只能说往下看。会出现#include嵌套:A.h里面引用了B.h,而你在test.cpp里面同时引用了A.h和B.h,这不就出现了吗

它其实和下面的搭配起到的作用是一样的

#ifndef XXX
#define XXX
//代码块
#endif

链接

变量

基础变量

变量类型不同,本质是变量的存储空间不同。

指针类型的变量大小与计算机的地址位数有关。

引用类型的变量大小 似乎与目标类型一致

指针

原始指针/传统指针

指针是一个无符号整数,存储的是内存地址,所以它的大小自然与计算机硬件——内存的地址位数有关。

通过*pointer来读写某个指针所指向的内存。指针的类型与计算机写入内存的具体操作有关。比如一个整数类型的指针读写是4个字节位单位的,char是1个字节等等

指针的指针的指针的。。。。像不像间接寻址?

智能指针

引用

引用本质上和指针一样,引用更像是基于指针的一种语法糖而已。也可以说,引用是变量的别名Alias

int a = 10;
int& ref = a;

实际编译之后,ref这个变量不会被创建,只有a会。

有了ref这个别名之后,你就可以像使用a一样使用ref


引用指向了一个实际变量之后,就不能更改

int b = 34;
ref = b;
//a = 34 b = 34

声明引用时必须要赋值,否则由于引用不能更改原则,这个引用会永远是空的。

函数

函数的名字,存储的是函数的起始地址。

头文件

通常来说,你如果想在一个文件里面调用另一个文件里定义过的函数,那么就必须在该文件里面先声明这个函数。那如果所有其他的文件也都需要调用这个函数呢?是不是要把这个声明到处写呢?答案是你就是要到处写。不过有一个方法能减少这个痛苦,那就是用#include包含进头文件

上面说过,#include其实就是复制粘贴功能,而写了一堆声明的,以.h结尾的文件就是头文件`。这俩搭配起来不就是把声明到处写么。


有的有.h 有的没有,这是怎么回事?你去问写c++标准库的那群人,他们就不带h。不过这也是区分c标准库与c++标准库的一个方法:c的标准库带h,c++的不带

类 vs 结构体

  • 成员默认访问权限不同
  • 类默认private 结构体默认public

C++中有结构体完全是为了兼容c

静态

类外/结构体外 static

类外被static修饰的内容,在链接阶段是局部的。它只对编译单元可见,以此解决不同文件中有相同名字的变量或函数的问题

类内/结构体内 static

类外被static修饰的内容是被这个类所有实例共享的。无论创建多少个对象,static内容始终只有一个。

静态方法内部无法访问到类的实例

静态变量必须要初始化?且初始化方式是类名::变量名,而且访问的时候,也是通过类名::变量名访问,也可以通过对象名.变量名访问,但不能通过类名.变量名访问。

虚函数

多态不仅发生在声明赋值变量的时候,也发生在参数传递中

//声明变量赋值
Child* c = new Child();
Father* f = c;
//参数传递
void fun1(Father& x)
{
    std::cout << x->name << std::endl;
}
fun(c);
//结果是输出Father的name而不是Child的name

为什么?因为cpp总是倾向于优先在指定的类里面寻找匹配的函数,这里就是优先在Father里面找name。为了让cpp明白继承中共有的成员,就有了虚函数

上面说的是本质,但在实际使用场景中的解释就是,父类声明原函数要用virtual修饰,子类可以重写原函数。C++11后有关键字override写在函数名后面,不过写不写程序都可以正常运行

//override
int getInt() override {}

虚表

虚函数是用虚表来维护的,所以使用虚函数有内存消耗;每次调用虚函数,cpp会遍历虚表找到最终要使用的函数,这有时间消耗。

纯虚函数

某些情况下,提供默认实现毫无意义,所以就有了只声明不实现的纯真版虚函数,即纯虚函数。

//纯虚函数的写法
class Base{
public:
    virtual void fun() = 0;
};

拥有纯虚函数的类无法被实例化。所以全是纯虚函数的类就像java里面的接口一样。

另类的C++数组

  • 声明方式另类
//这是Java
int[] a = new int[3];
//这是c++
int a[3];
  • Release模式下,越界不报错

数组初始化

int a[4];
int* a = new int[4];
//后面这种方式没法用sizeof(a) / sizeof(int)计算数组长度,因为sizeof(a)得到的不是数组总字节数而是指针字节数

上面两种初始化方法不同之处是:前者创建在栈上,跳出当前作用域会被销毁;后者创建在堆上,会一直存在直至整个程序结束。如果我们不手动清理这些堆上的内存的话,内存会被塞满的(Java和C#有垃圾回收机制不用手动清理)。于是就有了下面的内存管理

内存管理——管理堆内存

使用关键字delete释放内存

int* a = new int[3];
delete[] a;

C++ Style string表示字符串

string其实是由char数组组成的。而且字符串其实也可以用其他方式表示

//C style
const char* str = "Hello";
//const 只是个语言规范,只是我们的一个承诺:我们一定不会修改str这个字符串

注意,被双括号包围起来的文本本质上是char*而不是字符串。而std::string有接受char*const char*的构造函数

字符串的复制及其耗时

所以我们在传字符串的时候,为了缩短时间,经常采用const+引用的方式

void PrintStirng(const std::string& msg)
{
    std::cout<<msg<<std::endl;
}

const是承诺:我们永不修改msg 引用是节省开销:传参的时候不必复制一个字符串而是直接把字符串的地址传过来

参数是对象的时候,实际上是复制了这个对象的然后传给这个函数


char[] 与 char*

char* 实际上是把字符串当做一个常量存储在内存里面,然后这个char*就是一个指向这个常量地址的指针。所以其实无论你写成char*还是const char*你都不可以修改字符串。有些编译器就强制你写后者,不然报错。

char[]是可以修改的,但请你不要误会,字符串仍然是一个常量。那么为啥char[]可以修改呢?这是编译器在编译过程中帮我们做的。现在你只需要知道,实际上是编译器帮你声明了一个栈变量并把字符串的数据复制给了它,然后通过修改它的值来达到修改字符串的目的。

一个承诺:const

  • const int*
  • 承诺不改变地址存储的内容,但能改变指向的地址
  • int const*
  • 和const int*完全一样
  • int* const
  • 承诺不改变指向的地址,但能改变地址的内容

记忆方法:从右往左读。原理:*其实应该是和变量名紧连着的

int* a,b;//这里a是个指针,但b是个整数变量
int* a,*b;//这里两个都是指针了

mutable 可变的。

常量指针 指针常量
int x = 4;
	int y = 5;
	const int *p1 = &x; //无法更改内容 ,但能更改指向的地址 
	int const *p2 = &x; //无法更改内容 ,但能更改指向的地址
	int * const p3 = &x;//可以更改内容 ,但无法更改指向的地址
	const int * const p4 = &y;//无法更改内容 ,亦无法更改指向的地址

输入输出

cin

cin是一个iostream对象

cin>>a>>b;

上面和下面是等效的

cin>>a;
cin>>b;

文件输入输出

将文本全部输出。声明,打开,读取

#include <fstream>
char data[10];
std::ifstream infile;
infile.open("data.txt");
while(!infile.eof())
{
    infile >> data;
    std::cout << data << std::endl;
}

在内存上创建对象

内存有堆内存和栈内存之分。

栈内存上创建的对象,称为栈对象,

  • 它的生命周期由它生命语句所在的作用域决定。超出这个作用域他就被自动释放了。
  • 栈内存很小
  • 分配速度快

堆内存上创建的对象,称为堆对象,

  • 它的生命周期随程序结束而释放
  • 堆内存很大
  • 使用new关键字
  • 分配速度慢
  • 需要手动释放,释放用到delete关键字

栈对象

Class Entity{
private:
    string name;
public:
    Entity():name("DEFAULT"){}
    const string& GetName() const {return name;}
};
int main()
{
	//两种创建方式都可
    Entity e1();
    Entity e2 = Entity();
}

闲谈

一个典型的对栈对象理解不到位的错误就是,在一个函数里面做出分配操作然后返回这个地址,殊不知这个地址指向的内存在函数结束后就被回收了

堆对象

Class Entity{
private:
    string name;
public:
    Entity():name("DEFAULT"){}
    const string& GetName() const {return name;}
};
int main()
{
	Entity* e = new Entity();//没错这里要用到指针
}

为什么要用到指针

因为C++里面new 返回的是堆内存上的地址,不是一个引用。但是在C#和Java里面new 返回的是引用,并且C#里面所有的class都存储在堆上所有的struct都存储在栈上。而Java里面所有东西统统存储在堆上

智能指针

简单来说就是被包装的指针。

它们都能够实现:作用域结束时,堆对象被回收。甚至能自动new对象。没错他就是为了防止减轻人们使用new和delete的痛苦而诞生的。

作用域指针

简单来说就是被包装的指针。

能够实现:作用域结束时,堆对象被回收。原理如下

class ScopePtr
{
    private:
    Entity* ptr;
    public:
    ScopePtr(Entity* e)
        :ptr(e){}
    ~ScopePtr()
    {
        delete ptr;
    }
};

int main()
{
    {
        ScopePtr p = new Entity();
    }
}

总的来说就是,一个栈对象有一个指向堆对象的指针,但栈对象的析构函数具有销毁该堆对象的功能

unique_ptr

shared_ptr