C++学习简记Ⅱ

发布时间 2023-05-16 21:20:54作者: Starts-cat

C++数组

Array

可以在堆(heap)上创建一个数组


int* another = new int[5]; //其作用域与在栈上创建不同,直到程序把它销毁之前,它都是处于活动状态的,需要用delete关键字来删除

delete[] another;

使用new动态分配最大的原因是生存期,用new来分配的内存,它将一直存在,直到手动删除它。如果你有一个函数返回一个数组,你必须使用一个new关键字来分配它,除非你传入一个数组的地址参数。在堆上创建数组,该数组指针所指的内容为一个地址,这个地址指向数组的第一个元素。所以,应该在栈上创建数组来避免这种情况,因为像这样在内存中跳跃肯定会影响性能

另外,在栈上创建的数组可以用sizeof获得其大小,而在堆上创建的数组无法直接获得其大小,因为它只是一个地址,所以我们需要在创建数组时记录其大小


> C++11中有内置数据结构std::array,相较于原始数组有很多优点,例如边界检查,记录数组大小

``` C++

#include< array >
std::array< int, 5 > another;

C++字符串

字符串本质上是一个字符数组。

const char* name = "Cherno";
//char* name = "Cherno"这样的代码风格在C++11并没有被舍弃,但是不推荐使用。因为这样的代码会导致指针和字符串字面量类型不匹配的问题,可能会引发未定义行为。建议使用std::string或者const char来代替char。

在C++的标准库中有一个名为String的类,实际上有一个类叫BasicString,它是一个模板类,String是BasicString的一个特化版本,模板参数是char,它是一个字符数组的包装器,它提供了很多有用的方法,例如获取字符串长度,连接字符串,查找字符串,替换字符串等等

std::string怎么工作?它只是一个char数组,有一个char数组和一些函数,用来操作这个数组,它的长度是可变的

另一件常见的事是追加字符串,我们想做cherno + hello!不能写为


std::string name = "Cherno" + "hello!";

发生这种情况的原因是 你实际上是想将两个const char的数组相加(双引号里的东西是const char数组,不是真正的字符串),但是这是不可能的,这样实际上是两个指针相加。一个很简单的方法是把它分开成多行


name += "hello!";

这样做是将一个指针,加到了name。name是一个是字符串,你把它加到一个字符串上,+=这个操作在string类被重载了,所以可以这样写。

或者将两个相加的字符数组其中的一个,显式调用string的构造函数


std::string name = std::string("Cherno") + "hello!";

相当于创建了一个字符串,然后附加这个字符数组给它

字符串字面量

双字符串字面量是在双引号之间的一串字符

定义字符串时如果不使用const关键字而直接写类似char* name = "Cherno"这样的代码,这样的代码风格在C++11并没有被舍弃,但是不推荐使用。因为这样的代码会导致指针和字符串字面量类型不匹配的问题,可能会引发未定义行为。建议使用std::string或者const char来代替char。

原因是,你在这里所做的是,你取了一个指向那个字符串字面量的内存位置的指针,而字符串字面量是储存在内存的只读部分的

如果确实需要修改它,只需要将类型定义为一个数组,而不是指针


char name[] = "Cherno";

除了char 还有一种类型叫做wchar_t,这就是宽字符。定义时需要在前加上大写L,表示下面的字符串字面值由宽字符组成const wchar_t* name2 = L"Cherno"

C++也引入了一些其他类型,比如char16_t,需要在前加上u。char32_t,加上大写的U


const char16_t* name3 = u"Cherno";
const char32_t* name4 = U"Cherno";

基本上,char是一个字节的字符,char16是两个字节的16位的字符,char32是32位,4字节的字符分别对应utf8,utf16,utf32

那么wchar和char16的区别是什么?因为他们似乎都是两个子节点字符

虽然一直说每个字符是两个字节,然而这实际上是由编译器决定的,它可能是一个字节,也可能是两个字节,也可能是四个字节。在实际应用中,通常不是两个就是四个字节。在Windows上是2个字节,在Linux上是4个字节。所以这其实是一个变动的值。
如果你确实要的是2个字节的,就用char16_t。

再讲讲两个字符串的事情。比如字符串附加。在C++14,有个std::string_literals 给出了一些方便的字符函数

上文讲过,如果需要在一个字符串附加一些其他的字符串,不能使用


std::string name0 = "Cherno" + "hello";

因为这些都是字符串字面量,他们实际上是数组或指针。之前的解决方案是用一个构造函数将其包围起来,使其成为一个string对象。然而,因为在C++14的string_literals库中有办法可以让事情变得简单一点。

可以将s加到字符串的末尾。实际上这是一个函数。他是一个操作符函数,返回标准字符串(对象)

我们还可以使用另一种方法来附加字符串字面量——字母R

在字面量前写上R,这意味着忽略转义字符

在实际应用中,如果我们要打印的东西是有很多行的字符串,可以直接像下面写


const char* example = R"(Line1
Line2
Line3
Line4)";

如果不这样,我们就需要用附加字符串的方法或下面的方法


const char* ex = "Line1\n"
    "Line2\n"
    "Line3\n";

最后,关于字符串字面量的内存以及其如何工作

字符串字面量永远保存在内存的只读区域(略)

C++中的const

const在改变生成代码方面做不了什么,它有点像类和结构体的可见性,这是一个机制,让代码更加干净,并对开发人员写代码强制特定的规则。const像是你做出的承诺,他承诺某些东西将是不变的。然而,它只是一个你可以绕过的承诺。我们使用const的原因是这个承诺可以简化很多代码

const除了可以声明一个常量,还有其他几种用法

首先是指针,通常对于一个指针,我们可以修改其指向的内容和其地址

但使用了const(将const放在int*前)后,将无法修改指针指向的内容,但仍可以修改指针的指向(即地址)


const int* a  =new int;

使用const的第二种方式是将它放在*之后


int* const a = new int;

它的作用恰好相反,我们可以改变指针的指向,但无法把实际的指针本身重新赋值,指向别的东西

注意,下面两种写法的作用是相同的


int const* a = new int;
const int* a = new int;

当然,可以写两个const,这样既不能改变指针指向的内容,也不能改变指针本身


const int* const a = new int;

在类中以及方法中使用const

class Entity
{
    private:
        int m_X, m_Y;
    public:
    int GetX() const
    {
        return m_X;
    }
};

在类中方法的参数列表后写上const,这就是const的第三种用法。这意味着这个方法不会修改任何实际的类(即这个方法只能读取数据),所以我们不能修改类成员变量。如果在GetX中尝试m_X = 2则会出错

定义下面的函数


void PrintEntity(const Entity& e)
{
    std::cout << e.Get() << std::endl;
}

如果去掉GetX后的const,将不能调用GetX,因为GetX函数已经不能保证它不会写入Entity了。在这里,e是作为const ENtity的引用的,所以不能将e重新赋值。(这与参数为const Entity*类似)

正因如此有时你会看到函数的两个版本例如一个带有const一个不带


int Get() const
{
    return m_X;
}

int Get()
{
    return m_X;
}

如果你确实想要将方法标记为const,但由于某些原因,又确实需要修改一些变量。在C++中有一个关键词mutable 这个词意味着它是可以被改变的


mutable int var;

这样即使在const方法中也可以对var作出修改

C++中的mutable关键字

mutable有两种不同的用途,其中之一就是上文中的与const一起使用,另一种是用在lambda表达式中。

C++的成员初始化列表

构造函数初始化列表。这是我们在构造函数中初始化类成员(变量)的一种方式。
当我们编写一个类并向该类添加成员时,通常需要用某种方法来初始化这些成员。在C++中,有两种方法可以做到这一点。第一种是在构造函数中初始化它们,第二种是使用成员初始化列表。这两种方法都可以做到这一点,但是成员初始化列表的效率更高,因为它可以避免不必要的构造函数调用。


class Entity
{
    private:
        std::string m_Name;
        int m_Score;
    public:
        Entity()
            :m_Name("Unknown"),m_Score(0) //成员初始化列表
            {

            }
}
    

要注意的是,在成员初始化列表中,应该按照声明的顺序写。因为不管你怎么写,都会按照声明的顺序初始化。如果你打破这个顺序,这就会导致各种各样的依赖性问题。

我们为什么要使用这个?首先这会让代码看起来整洁,使构造函数干净易读。还有一个功能上的区别,在特定类的情况下,如果写如下的代码


class Entity
{
    private:
        std::string m_Name;
        int m_Score;
    public:
        Entity()
            :m_Name("Unknown"),m_Score(0) //成员初始化列表
            {

            }
}