C++ 简单介绍

总结一下之前使用C++的笔记。

包含目录

1
2
#include "" 在整个工程包含目录范围内搜索。
#include <> 在系统环境中搜索。

指针

void 作为无类型指针,有类型可以隐式转换成void指针,而void*不能转至有类型指针。

以下是通过的

1
2
3
void *p1;
int *p2;
p1 = p2;

以下是不通过的

1
2
3
void *p1;
int *p2;
p2 = p1;

指针在数组中的表现:

1
2
3
指针占用4个字节
int *a = 10;
a+3; //往后推12位

1
2
3
4
5
6
所以在数组中,+1即是下一个索引,可能会越界。
int data[10];
int *ptrData = &data[0];//首个元素指针变量
//下面两种方式循秩访问
int val_i = data[i];
int also_val_i = *(ptrData+i);

非常量指针指向常量数据

1
2
int data = 10;
const int *ptrData = &data;

常量指针指向非常量数据

1
2
int data =10;
int * const ptrData = &data;

常量指针指向常量数据

1
2
int data = 10;
const int * const ptrData = &data;

类型转换

更为细致的总结:
https://blog.csdn.net/chenlycly/article/details/38713981

数据在内存中是没有什么类型的,我们定义的变量类型,其实就是在特定地址范围内,定义了数据“应该被看成什么”的方式。 比如char转int,宽化转化;double转int,窄化转换。

在继承关系上,上行转换一般来说是安全的,下行转换很可能不安全,子类是包含父类的。

简述:
dynamic_cast与static_cast的区别:
1、static_cast在编译期间检查,dynamic_cast在运行期间检查。
2、dynamic的type要求为指针或者引用,或者是右值,并且下行转换要求基类是多态的(起码有一个虚函数)
3、dynamic在运行期间可以识别出不安全的转换,并且抛出空指针,但不抛出异常。

常量转非常量,不是针对原常量本身,而是指返回结果不是常量。

char

在C里面Char c是作为字符串来处理的,一个隐式的c[],
在C++里面char
必须标记为const,他是存在静态存储区的,程序结束才会释放。
所以在C++里面会是这样:

1
2
3
char c1[5] = "game";
const char* c2 = "game";
char* c3 = "game";//在C++里面是会报错的。

std::unique_ptr

智能指针,脱离作用域后自动释放。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>
struct B {
virtual void bar() { std::cout << "B::bar\n"; }
virtual ~B() = default;
};
struct D : B
{
D() { std::cout << "D::D\n"; } //构造函数,初始化时
~D() { std::cout << "D::~D\n"; } //析构函数,销毁时
void bar() override { std::cout << "D::bar\n"; }
};
// 消费 unique_ptr 的函数能以值或以右值引用接收它
std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
{
p->bar();
return p;
}
int main()
{
std::cout << "unique ownership semantics demo\n";
{
auto p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
auto q = pass_through(std::move(p));
assert(!p); // 现在 p 不占有任何内容并保有空指针
q->bar(); // 而 q 占有 D 对象
} // ~D 调用于此
std::cout << "Runtime polymorphism demo\n";
{
std::unique_ptr<B> p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
// 作为指向基类的指针
p->bar(); // 虚派发
std::vector<std::unique_ptr<B>> v; // unique_ptr 能存储于容器
v.push_back(std::make_unique<D>());
v.push_back(std::move(p));
v.emplace_back(new D);
for(auto& p: v) p->bar(); // 虚派发
} // ~D called 3 times
std::cout << "Custom deleter demo\n";
std::ofstream("demo.txt") << 'x'; // 准备要读的文件
{
std::unique_ptr<std::FILE, decltype(&std::fclose)> fp(std::fopen("demo.txt", "r"),
&std::fclose);
if(fp) // fopen 可以打开失败;该情况下 fp 保有空指针
std::cout << (char)std::fgetc(fp.get()) << '\n';
} // fclose() 调用于此,但仅若 FILE* 不是空指针
// (即 fopen 成功)
std::cout << "Custom lambda-expression deleter demo\n";
{
std::unique_ptr<D, std::function<void(D*)>> p(new D, [](D* ptr)
{
std::cout << "destroying from a custom deleter...\n";
delete ptr;
}); // p 占有 D
p->bar();
} // 调用上述 lambda 并销毁 D
std::cout << "Array form of unique_ptr demo\n";
{
std::unique_ptr<D[]> p{new D[3]};
} // 调用 ~D 3 次
}

数组的动态内存申请

1
2
3
int *data = new int[5]; //返回的是首个元素在数组中的地址
delete data ; //只是删除首个元素
delete [] data; //删除整个数组,正确的释放内存

类的定义

ClassName.h

1
2
3
4
5
6
7
8
9
10
11
class ClassName{
Public:
ClassName(args);//构造器
...
~ClassName();//析构函数
type name; //公共变量
type method(args); //公有方法
private:
type name; //私有变量
type method(args); //私有方法
};

ClassName.cpp

1
type ClassName::method(args){} //方法实现

  • 正常new派生类指针,会先调用基类构造再调用派生类构造。
  • 正常delete派生类指针,和构造相反,先调用派生类析构,再调用基类析构。

构造器

左值引用,右值引用

1
2
3
4
5
6
7
8
9
void printInt(int& i){cout << "lvalue reference" << i << endl;}
void printInt(int&& i){cout << "rvalue reference" << i << endl;}
int main(){
int a = 5;//a is a lvalue
int& b = a; //b is a lvaule reference
int&& c ;// c is a rvalue reference
printInt(a); //call printInt(int& i)
printInt(6);//call printInt(int&& i)
}

构造函数

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
class boVector{
int size;
double *arr_;
public :
boVector(const boVector& rhs){ //Copy 深拷贝,如果不定义移动构造函数,当传入一个右值时,就会进来这里,const T& 对应的可以是左值也可以是右值。
size = rhs.size;
arr_ = new double[size];
for(int i =0;i<size;i++){arr_[i]=rhs.arr_[i];} //一个一个复制
}
boVector(boVector&& rhs){ //T&& 接收右值,移动构造函数,右值
size = rhs.size;
arr_ = rhs.arr_;
rhs.arr_ = nullptr;
}
~boVector(){ delete arr_; }
};
void foo(boVector v);
void foo_by_ref();
boVector createBoVector(); //creates a boVector
void main(){
boVector reusable = createBoVector();
foo_by_ref(reusable);//call no constructor
foo(reusable); //call copy constructor
foo(std::move(reusable)); //call move constructor
}

pragma once

使用#pragma once代替include防范将加快编译速度,因为这是一种高级的机制;编译器会自动比对文件名称或iclude而不需要在头文件去判断#ifndef和#endif。

grandparent.h

1
2
3
4
5
#pragma once
struct foo
{
int member;
};

parent.h

1
#include "grandparent.h"

child.c

1
2
#include "grandparent.h"
#include "parent.h"

main(arg[] …)函数的入口参数,可以在VS解决方案属性中配置

小tips

CPP和C#不同的地方是

1
2
3
4
5
xxxClass c; //声明一个变量,其中就调用了他的空构造函数
C#里面通常是
xxxClass c3 = new xxxClass(1,2); //在C++里面有new就要有delete,不然脱离了作用域他是不会自动释放的。
C#分值类型和引用类型,struct,class,CPP不分,全部都是值类型。
引用其实是指针的别名。

何时使用指针和引用:

1、引用意味着必须指向一个对象,不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。

1
2
3
4
5
6
7
8
9
10
11
12
void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它肯定指向一个double值
}
//相反,指针则应该总是被测试,防止其为空:
void printDouble(const double *pd)
{
if (pd)
{ // 检查是否为NULL
cout << *pd;
}
}

2、指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变(这里是指引用关系不变)。

1
2
3
4
5
6
7
8
string s1("Nancy");
string s2("Clancy");
string &rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是"Clancy",s1被更改。
ps = &s2; // ps 现在指向 s2;指针只是换换指向而已,并没有对内存单元中的值进行操作
//s1 没有改变

3、当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[].这个操作符典型的用法是返回一个目标对象,其能被赋值。

1
2
3
4
5
vector<int> v(10); // 建立整形向量(vector),大小为10;
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
//如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10; //此时只是为了说明问题而写的,编译器不一定能通过,有语义误解
// 但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。

总之,当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。能使用引用的地方都能使用指针,反之不成立。

4、std::move把引用做了一个升级,类似指针传递。C++11之前,没有这个方法。由于引用在初始化的时候必须指定对象,通常可以重写“=”操作符,根据自己的需要改成拷贝构造或者移动构造。

char

1
2
3
char c1[5] = "game";
const char* c2 = "game"; //其实是字符串第一个元素的地址。
char* c3 = "game";//在C++里面是会报错的。C里面是允许的,主要是为了保证没有const以前的C代码可以编译通过,但是指向的内容不能被修改,这是需要程序员自己来控制的。
1
2
3
4
5
6
7
8
9
10
11
12
string GetStr()
{
return "test";
}
int main()
{
string str = GetStr();
const char* c1 = GetStr().c_str(); //临时对象调用后,被析构
cout << c1 << endl; //乱码
const char* c2 = str.c_str();//临时对象接收,延长作用域
cout << c2 << endl; //正确
}

CPP的委托

或者可以叫做函数指针。
如GLFW中的

1
typedef void (* GLFWmousebuttonfun)(GLFWwindow*,int,int,int);

定义按钮输入事件函数的标准。

delete[]

1
2
delete []objects; // 正确的用法
delete objects; // 错误的用法

后者相当于delete objects[0],漏掉了另外99 个对象
严格应该这样说:后者相当于仅调用了objects[0]的析构函数,漏掉了调用另外99 个对象的析构函数,并且在调用之后释放内存时导致异常(如果存在析构函数的话),如果对象无析构函数该语句与delete []objects相同

参数传递

cpp的参数传递,如果传的是值,存在一个拷贝。如果传的是引用,则不存在拷贝,同时在传引用的时候,最好明确是否为const。

返回值

1
2
3
4
5
6
7
8
Rigdbody Player::GetRigdbody() //把拷贝值返回,注意,这返回的是一个右值
{
return rd;
}
Rigdbody& Player::GetRigdbody() //返回引用,为地址
{
return rd;
}

不要返回局部变量的引用.

因为局部变量在脱离作用域后,会被释放,对这块地址没有所有权,之后可能发生的事情未知。

1
2
3
4
5
6
7
8
9
10
11
12
int& func1()
{
int i;
i = 1;
return i;
}
void main() {
int& p = func1();
std::cout<<p<<endl; //1,局部变量的引用释放,对应地址的值目前还没有发生改变
//执行若干代码后,之前p地址对应的内容已经被修改
std::cout<<p<<endl; //不正确的结果
}

关于多返回值

  • 0、定义一个结构体,把需要的返回对象写进去,这也是最为清晰的代码书写。
  • 1、可以是以指针或者引用作为参数,用于做返回值。
  • 2、如果是同一类型的返回值,可以让他返回个数组,或者使用一些模板类,或者自己定义,比如std::array,std::pair以此作为返回对象。
  • 3、如果是多个不同类型的变量,#include一下tuple,可以使用std::tuple
    1
    2
    3
    4
    std::tuple<int, Point> tt(123, ap);
    auto t = std::make_tuple(123, ap);
    int i = std::get<0>(tt);
    Point p = std::get<1>(t);

static

类中定义的Static变量,在类实例化之前已经分配了内存地址

函数内的Static的变量,可以用于记住上一次调用函数时的变量状态。