CPP 基础知识(一)
author:dr@g0n
由于一些原因需要用到cpp的知识,所以在此处记录学习,更多更新内容请看yuque(https://www.yuque.com/along-ojern/mob3of/psynut)
此学习小结,更具Cherno的youtube教程总结而来,他的cpp教程简洁易懂,十分适合有一定开发编程经验(最好懂点汇编和英语)的同学(https://www.youtube.com/watch?v=18c3MTX0PK0&list=PLlrATfBNZ98dudnM48yfGUldqGD0S4)
Visual Studio
创建项目后,vs会自动生成一些文件.
sln solution 文件 保存项目结构
filters
vcxproj
vs自动生成的文件结构非常混论,建议点击显示所有文件按钮后,添加src文件夹存放源代码。
常用项目配置
编译器
一、预编译 preprocessor
1、#define
#define INTEGER int
//编译器在预编译时会将define修饰的内容全部替换,
//此处例子中会将INTEGER替换为int
INTEGER Multiply(INTEGER a,INTEGER b){
}
2、#include
编译器在预编译时会将include的文件内容复制到当前文件中。cpp标准库没有文件后缀,例如iostream ,而c语言标准库又.h后缀,例如stdlib.h。
#include<iostream> //特点include路径用<>
#include "test.h" //相对路径用""
3、#if endif
#if condition
blablabla //只有当condition为真时,此内容才会被编译
#end if
4、#pragma once
只include该文件一次,往往出现在.h文件中,防止头文件被include到同一个cpp中多次。可以代替以前#ifndef #endif的作用,起到头文件保护的作用。
#ifndef _FLAG_H
#define _FLAG_H
blablabla //当该文件被include一次后_FLAG_H就已经被define了,
//如果第二次被include,blabla内容就不会执行
#endif
二、编译为obj (translation unit)
obj file 就是一堆只有cpu认识的二进制数,可以在项目属性中更改输出文件,让编译器输出asm代码,使其具有可读性
链接器 Linking
把多个obj文件,及程序所要用到的库链接在一起
.h 文件放函数declaration
#pragma once
using namespace std;
void countStr(const string str);
.cpp放函数
#include <iostream>
#include <string>
#include "countStr.h"
using namespace std;
int main(char argc, char** argv) {
string str;
cin >> str;
countStr(str);
}
#include <iostream>
#include <string>
using namespace std;
void countStr(const string str) {
for (int i = 0; i < str.length(); i++) {
cout << i << endl;
}
}
头文件 .h
一般用来放函数声明,然后就可以在需要调用的地方#include “***.h”
指针(Pointer)
指针是cpp中最重要的概念之一,本质上来说指针是一个存储变量内存地址的整数,指针类型对指针本身并无任何意义,他只是告诉编译器指针指向变量的大小。
#include<iostream>
#define LOG(x) cout<<x<<endl;
using namespace std;
int main(int argc, char** argv) {
char* buffer = new char[8]; //在堆(heap)内创建8字节空姐
memset(buffer, 0, sizeof(buffer));//将buffer置为0
LOG(buffer);
char** buffer_ptr = &buffer;//指向指针buffer的指针
LOG(buffer_ptr);
**buffer_ptr = 10;//改变的是指向buffer指针对应的值,从0变为10
LOG(buffer);
}
#include<iostream>
#define LOG(x) std::cout<<x<<std::endl;
int main(int argc, char** argv) {
char* buffer = new char[8];
memset(buffer, 65, sizeof(buffer));
LOG(buffer); //输出指针指向的8字节内存中的值,因为是char,所以用ascii解析
char** buffer_ptr = &buffer;
LOG(buffer_ptr);
**buffer_ptr = 97;
LOG(*buffer); //只输出指针指向的内存地址的值,因为是char类型,只有1字节,并用ascii解析
}
cpp中的const只修饰其右边的内容
const int* a = new int;
b=2;
*a = 2;//报错,因为const修饰了*a其为常量,即不能更改指针指向地址的值
a = (int*)&b;//不报错,这里我们更改了指针指向的地址
int* const a = new int;
*a = 2;//不报错,因为const修饰了a即指针
a = (int*)&b;//报错
引用(reference)
cpp中的引用本质上就是指针,是削弱后的指针。引用必须引用一个已存在的变量,即必须初始化,并且初始化后不可改变。引用本身不是变量,可以把他当成变量的别名。
int& ref = a; //ref必须先初始化,即赋一个变量。
面向对象编程
一、类(class)
本质上说class是一种将数据(data)和函数(methods、funcitons)组织在一起的数据结构。class中的成员变量默认访问控制为private。
#include <iostream>
class Player {
public:
int x;
int y;
float speed;
void Move(int xa, int ya) {
x += xa * speed;
y += ya * speed;
}
};
int main(int argc, char** argv) {
Player player1;
player1.x = 1;
player1.y = 1;
player1.speed = 3;
player1.Move(2, 2);
std::cout << player1.x << std::endl;
std::cout << player1.y << std::endl;
}
1、类(class)与结构体(struct)区别
类默认成员变量为private,而struct默认为public。一般情况下,struct是数据的结构体,仅仅用于存放pod(plain old data),例如数学中的向量Vector概念就可以用struct编写。简而言之,struct和class其实可以相互替换,但是为了代码可读性及逻辑通顺,不建议这样做,struct就让他处理一些不复杂(逻辑上的)的数据。
struct Vec{
float x;
float y;
void Add(Vec& other){
x+=other.x;
y+=other.y;
}
};
2、static关键字
static可以放在类和结构体中,也可放在之外。
static在类外表示,static修饰的符号在link(链接)阶段是局部的,即只对定义它的编译单元可见(.obj)。
//Static.cpp
int s_Var = 5;
//Main.cpp
int main(){
int s_Var = 10;
// crtl+f7 编译单个文件不会有问题,但是f5编译并链接项目时,由于Static.cpp中已经存在s_Var
//因此编译器会报“找到一个或多个多重定义的符号”错误,但是当我们修改Static.cpp中s_Var为static时
//由于static修饰的符号在link(链接)阶段是局部的,即只对定义它的编译单元(.obj)、声明他的cpp文件可见
//因此不会报错。
//ps(如果我们想在一个cpp中调用另一个cpp中的全局变量可以用extern关键字,该关键词可以让编译器在另外的编译单元(obj)中找定义,相当于变成了该变量的引用)
}
总结:尽量让全局变量和函数static,除非你想在其他cpp文件中要到他。
static在类和结构体中表示,这部分内存是由这个类的所有实例共享的。静态方法里没有该实例的指针,即this。
#include <iostream>
class Singleton {
public:
static Singleton& Get() {
static Singleton instance;
return instance;
}
void Hello() {
std::cout << "hello" << std::endl;
}
};
int main(int argc,char** argv){
Singleton::Get().Hello();
}
//第二种返回Singleton实例的方式
class Singleton {
public:
static Singleton* instance;
static Singleton& Get() {
return *instance;
}
void Hello() {
std::cout << "hello" << std::endl;
}
};
Singleton* Singleton::instance = nullptr;
int main(int argc,char** argv){
Singleton::Get().Hello();
}
cpp中每个类中的非静态方法总是将当前类的实例当作参数传入。因此静态方法无法读取类中的非静态变量。
对于类需要用到但是类实例之间不变的变量可以把他设置为static。
static void Print(Entity e){ // 非静态方法本质上是会传入当前Entity实例
std::cout<<e.x<<endl;
}
3、构造函数(Constructors)
实例化对象时会运行,与类的名称一致。默认情况下cpp中的类会有一个空的构造函数,但是你也可以删除它, Log()=delete;
4、析构函数(Destructors)
对象(实例)被摧毁时调用,当你在堆(heap)上开放内存空间时,你必须手动销毁他,此时析构函数就很有用了。
class Demo(){
public:
int a;
int b;
Example e;
//一定用下面这种方式初始化构造函数,可以节约资源
Demo(int aa,int bb):a(aa),b(bb),e(Example(8)){}//构造函数,这里时cpp的赋值语法,这里Example只会被创建一次
Demo(int aa){ //与上面类似,但不完全等价,在这里Example会被创建实例两次,前一次的实例被Example(8)构造函数覆盖掉
e=Example(8)
a=aa;
}
~Demo(){}//析构函数
};
5、继承
类之间存在经常需要复用的内容时可以用继承。可以先创造一个基类(模板),再用子类继承它。
class Base{
public:
float x;
float y;
void BaseFunc(){std::cout<<"base func"<<std::endl;}
};
class Son : public Base{
public:
char* name;
void SonFunc(){std::cout<<"Son func"<<std::endl;}
};
6、虚拟函数(Virtual Function)
虚拟函数起到动态调度的作用,会编译一个V-table,v-table会保存一个基类中所有虚拟函数的映射关系。如果需要override一个函数,你需要再父类中的那个被复写的函数前加上关键词virtual。
#include<iostream>
#include<string>
class Entity {
public:
virtual std::string GetName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_name;
public:
Player(const std::string& name) :m_name(name) {}
std::string GetName() override { return m_name; }
};
void printName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main(int argc, char** argv) {
Player* p = new Player("Dragon");
Entity* e = new Entity();
printName(e);
printName(p);
}
7、纯虚拟函数(接口)
cpp没有interface关键字,但是可以把纯虚拟函数当接口的抽象方法,继承自纯虚拟函数所在类的子类必须实现纯虚拟函数,才能创造实例。
class Entity {
public:
virtual std::string GetName() = 0; //让virtual 函数体=0即为纯虚拟函数。
};
class A{
public:
std::string GetName()override{} //必须实现该纯虚拟函数
};
8、public、private、protected
private对子类都不可见,且被private修饰的变量和方法在外部不能被调用。
protected对子类可见,但不能在外部被调用。
9、const、mutable关键字
当类方法不应该修改类,那么记住将方法标记为const,这样就保证调用者不能利用方法修改类中的内容
class Entity{
private:
int X;
mutable int var;
public:
int getX() const{ //这里的const,代表getX函数为read-only函数,不能对X有任何改变,即X=2,会报错
var = 2;//这里不会报错,因为var被mutable修饰,使其可以在const函数中被更改。
return X;
}
10、创建实例方法
分配给堆会比分配给栈使用更多时间,同时堆需要我们手动销毁。只有在class非常大和我们需要手动控制销毁时刻时再用new。
{
Entity e = Entity("dragon"); //栈上创建实例,随着栈被销毁而消失
}
{
Entity* e = new Entity("dragon"); //new堆上创建实例,只有我们delete了才会销毁,注意new返回的是实例所在的地址,即指针
std::cout<<e->getName()<<std::endl;
}
delete e;//需要我们手动销毁
二、特殊类
1、Enum(枚举类)
enum 相当于给了integer一个名字
enum Test: unsigned char{ //默认是4字节int ,不能是浮点类型
};
//eg.log 类
class Log {
public:
enum Level {
LevelError, LevelWarning, LevelInfo
};
private:
Level m_LogLevel= LevelInfo;
public:
void setLogLevel(Level LogLevel) {
m_LogLevel = LogLevel;
}
void Error(const char* messages) {
if(m_LogLevel>= LevelError){
std::cout << "[Error]" << messages << std::endl;
}
}
void Warn(const char* messages) {
if (m_LogLevel >= LevelWarning) {
std::cout << "[Warn]" << messages << std::endl;
}
}
void Info(const char* messages) {
if (m_LogLevel >= LevelInfo) {
std::cout << "[Info]" << messages << std::endl;
}
}
};
int main(){
Log log;
log.setLogLevel(Log::LevelInfo);
log.Error("error msg");
log.Warn("warn msg");
log.Info("info msg");
}
数据结构
一、数组(Array)
1、创建数组
static const int exampleSize=5
int example[exampleSize] = {1,2,3,4,5}; //数组在栈中,随着函数销毁而销毁
std::array<int,5> example2;//cpp11后可以用此种方式,更安全但是开销更大
int* example3 = new int[5]; //数组在堆中,需要手动销毁
delete[] example3; //销毁用new创建的数组
例子中的example实际是指针*(example+2)=5
与example[2]=5
等价。
如果你在函数中创建并需要返回一个数组,那必须使用new关键词创建。
数据类型
一、字符串(string)
字符串本质上就是字符的集合,
"dragon" // 类型为const char[7] ,之所以是7是因为有一个\0截止符号
char* name = "dragon";
char[7] name2 = {'d','r','a','g','o','n',0};//0是字符串的截止符号
std::string name = std:string("hello") + "drasgon";
std::string name = "hello"s + "drasgon";
//常用方法
name.find("a");//查找字符出啊
bool contains = name.find("a") != std::string::npos;
name.size();//判断大小
char* a = "qwe";
const char* a = "qwe";//正确写法
char* a = (char*)"qwe";//正确写法
a[2] = 'a';//这样的操作是不被允许的,因为qwe是字符串文本保存在内存中的“只读”部分.
//char 每个字符1个字节 utf-8
const char* name1 = u8"dragon";
//wchar_t 每个字符2个字节
const wchar_t* name2 = L"dragon";
//char16_t 每个字符2个字节 utf-16
const char16_t* name3 = u"dragon";
//char32_t 每个字符4个字节 utf-32
const char32_t* name4 = U"dragon";
//可以整段打印
const char* example = R"(line1
line2
line3
line4)";
二、整数类型
1字节(byte)=8比特(bit)=2个16进制数
cpp是一门很强大的语言,仅有非常少的规则限制,对于变量来说cpp变量之间唯一的区别就是大小。
1、变量大小
char 1字节
int 4字节
short 2字节
long 4字节
long long 8字节
float 4字节
double 8字节
bool 1字节
float a = 5.5f;
double a = 5.5;