C/C++复习回忆整理_incomplete

文章目录

前言

时间过得真快啊,不知不觉都要工作三年了。工作时使用的是c语言,c++都忘得干干净净了。其实我是有点抗拒c++的,过于复杂的语言特性让人望而却步。。。目前除了c++,我还想学习的语言有:java,python,go,sql这几个。
java:工作机会多,好像是后端用的,可以面向对象
python:使用率居高不下,易用强大的各种库,感觉可以和别的语言配合,搞点事情
go:背靠亲爹google,新兴的宠儿
sql:学习下数据库
除了这几种外,可能我还会去了解一下rust
额,扯远了,看了看草稿箱里的十三篇开了个头的文章,这填坑不知道要填到啥时候去,vr真好玩。
其实关于c/c++复习整理,以及比较和c/c++异同的工作,我从之前就开始计划了,然而打开the c programming language,发现要看的东西还是不少的,然后就搁置了。
有一种说法叫make it down,get it down,get it better。先开始做比较重要。

c

预处理器

三大预处理:文件包含,条件编译,宏定义
宏定义可以多次迭代,比如

//最后foo_a会被替换为foo_c
#define foo_a foo_b
#define foo_b foo_c

另外宏定义中还有#和##,#可以将宏参数转字符串,##可以将宏参数连起来,比如
有一个c程序宏为#define myprint(foo) #foo##test,如果调用myprint(123),那么最后替换的结果是123test.

类型

c语言中得内容有类型,变量,运算符,表达式,控制流,函数,指针等,其中除了char,int等,我把枚举,数组,结构体也归到类型里面去了。其实就和编程语言小记里面说的一样,由于要处理的东西是人类世界的,由此编程语言的各种要素也都是用机器模型体现的现实问题模型–基本所有编程语言都要涉及数据类型这个概念,抽象了二进制机器模型为问题模型。c语言的数据类型是需要提前声明的,c语言是静态的,弱类型的(这个存在争议),编译型语言。数据类型基本就是一块固定大小内存空间的,一组二进制编码的含义,以及其上支持的操作的集合。
结构体的合法操作只有:1.作为一个整体复制和赋值2.通过&取地址访问其成员。其中1用于想函数传递参数及从函数返回值,结构体之间不能进行比较。

变量

至于变量,可以算作是数据类型的实例化,提到整数,他就是一个概念,而具体来说,就是1,2,3这些,变量是可变的量,其实上面那句关于数据类型的解释更适于用在变量身上,变量是实实在在在内存中占用空间的,而数据类型,只是一种概念而已。有可变的量,也有不可变的量,即常量,其实就是不能改变的值,或者不需要去改变的值。
另外还有全局 变量,局部变量,局部静态变量,寄存器变量等等类型的变量,他们的默认初始化表现也是不一样的。需要注意的是,寄存器变量不能取地址,因为他并不是存储在内存中,而是寄存器中。

运算符

运算符,其实是可以看做与数据类型关联的方法。我们在现实世界可以3除2,但一般不会两个汉字相除,每种数据类型有其方法。运算符的本质就是对实例对象进行作用。

表达式

表达式:变量和运算符组成构成表达式,每个表达式都有一个值。左值和右值是两个很奇怪的词汇,这里的左和右的参照物应该是等号。一个表达式中,左边的对象接受了右边的值,或是说将右边的值赋值给了左边的对象。既然是赋值,那就代表左边的值是一个拥有内存空间的对象,而右边不必,比如常数可以作为右边的对象。这两个对象可能就代表左值与右值,左值可以为右值,因为左值既有内存空间,又有值,而右值可能不能作为左值,比如常数,只有值,但没有内存空间的概念,你要是想给常数赋值肯定就错了。这里引用一下:

总结:能产生左值的表达式就4个:
[ ](下标引用)
.(访问结构成员)
->(访问结构体指针成员)
*(间接访问)
需要使用左值表达式的有:访问结构成员,自加自减, 取地址。

语句

c语言中的常规语句是表达式加分号。另外还有一种是复合语句,也叫程序块,复合语句是用花括号{}把一组声明和语句括在一起构成的。右花括号用于结束程序块,不需要分号。

控制流

控制流,三种分类:顺序,分支,循环。一个表达式后面加上分号就成了一个语句。一条一条不包含分支和循环的语句,应该也算是顺序结构了。分支和循环则代表了选择和重复。

函数

函数:函数是一种封装,是可以复用的部分的提取。

指针

指针:c语言的精髓所在,是沟通上层与下层的桥梁,是抽象与具象的交界。c程序设计语言是这么描述指针的:

指针是一种保存变量地址的变量。

指针是一个抽象化的东西,而地址是具象化的,实实在在的代表了内存中得晶体管。指针全称应该叫指针变量,首先,他是一个变量,和char,short一样,他的内部能存储数据,而指针存储的数据是一个地址。

其他

恩,我是按照c程序设计语言的目录来罗列的,此书的最后两章内容为:输入与输出、UNIX系统接口。应当是讲了一些标准库的东西以及在操作系统上的c语言编程。因为此处只涉及语言本身,所以不讨论相关内容。这里只是一个大概的梳理,实际还有很多内容未涉及,比如变量作用域,预处理,结构体自引用等。这里其实是为了看c++的时候不要搞混了,我总是有这个担心。。

c++

c++与c的区别

到c++了,最近去面一个c++的岗位,总感觉面试官没问我太多的技术问题,很奇怪,虽然我提前说了我c++因为时间比较长,忘得差不多了,但是也只问了我一个关于c++的问题,也算老生常谈吧,问题是c和c++的区别有哪些。关于这个问题,我脑海里想到的第一件事情就是面向对象。
c语言是过程式编程语言,他的语言特性在我看来就是去一步步,一条条的把一件事情说明白。以分析设计好的数据结构为骨,以各种数据为血液,以针对数据结构和数据的进行处理的操作为皮肉,构成一个完整的程序。
而面向对象的编程方法,引入了一种新的名词叫做抽象,他是以这样的思想为支撑的:世间万事万物之间都有联系,他们是一个个分立的个体,同时又都拥有各种各样的关系。
通过抽象,数据和操作之间不再是松散的两种东西,他们可以是属于一个对象。一个对象有其自己的数据,这是他的属性,比如人有自己的名字。一个对象也有自己的方法,这是对对象的数据的操作集合,比如每个人可以说出自己的名字。对象与对象之间有联系,比如小明是一个学生,他是一个人类,那么小明和人类之间简单来说就存在一个从属关系。小明是人类这个宽泛概念的实例化,是一个具体的人而不是虚拟的一个名词。小明属性之一是名字,方法之一是说出自己的名字。这里可以看到关于面向对象语言的一个神奇特性,继承。继承可以看做是一种重用,有人说我在c语言里也可以做到这点,但是c++有许多的语言特性来帮你更轻易的实现这一点。不仅仅是继承,面向对象语言的另外两个要素是封装与多态。封装把对象,数据,方法绑定到了一起,并可以加以区分,保护。比如小明,只有他爸妈和自己才能给自己改名字,别人不能说改就改了,而别人可以叫小明的名字。多态,可以类比生物多样性,他在继承父类属性方法时又获得了一种灵活性,东北虎,华南虎,非洲虎,他们都要进食,但他们的食谱和捕猎方法可能不尽相同,在对象特化为实例对象后,具体的细节也需要一一落实,多态的作用大概如此。
面向对象语言使得看待编程的方式发生了变化,这种关于对象关系的提取发挥了复用的威力,在GUI编程,各种各样的窗体,按钮,搜索栏等等都有了联系,相比于过程式编程语言,在GUI编程中面向对象的编程语言可能更具优势。
当然除了面向对象,c++还支持泛型编程,函数式编程等。

基本概念

封装:隐藏实现细节,使得代码模块化
继承:扩展已存在的代码模块(类)
多态:接口重用,C++的多态性是通过虚函数来实现的
虚函数:不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
纯虚函数:代表函数没有被实现。需要在子类中去实现。比如基类的排序打印函数,实现不知道如何排序的。
重载:在同一个类中,函数名相同,参数列表不同,编译器会根据这些函数的不同参数列表,将同名的函数名称做修饰,从而生成一些不同名称的预处理函数,未体现多态。
重写:也叫覆盖,子类重新定义父类中有相同名称相同参数的虚函数,主要是在继承关系中出现的,被重写的函数必须是virtual的,重写函数的访问修饰符可以不同,尽管virtual是private的,子类中重写函数改为public,protected也可以,体现了多态。
重定义redefining:也叫隐藏,子类重新定义父类中有相同名称的非虚函数,参数列表可以相同可以不同,会覆盖其父类的方法,未体现多态。
友元:访问类私有成员的一种方法

命名空间

作用:防止定义冲突
using namespace::name 来声明使用特定命名空间的符号。
using namespace namespeace_name 所有来自namespeace_name的名字都可以使用。

标准输入输出对象

cin cout cerr clog


Q:c++的cin、cout、cerr和clog的作用是什么
chatgpt:
C++将输出分为cin、cout、cerr和clog四种的目的是为了提供不同类型的输出流,以满足不同的需求和场景。

cin:cin是C++中的标准输入流,用于从用户输入设备(通常是键盘)读取数据。通过cin,程序可以等待用户输入,并将输入的数据读取到程序中进行处理。

cout:cout是C++中的标准输出流,用于向标准输出设备(通常是控制台)输出数据。通过cout,程序可以将结果、消息或其他信息输出到控制台,供用户查看。

cerr:cerr是C++中的标准错误流,用于向标准错误设备(通常也是控制台)输出错误信息。与cout不同,cerr专门用于输出程序的错误信息,例如运行时错误、异常信息等。与cout相比,cerr的输出通常不会被重定向或缓冲,以确保错误信息能够及时显示。

clog:clog是C++中的标准日志流,用于向标准输出设备(通常是控制台)输出程序的日志信息。与cout相比,clog的输出通常会被缓冲,以提高性能。clog主要用于输出程序的运行状态、调试信息或其他日志记录,方便程序员进行调试和排查问题。

通过将输出分为不同的流,C++提供了更灵活和精确的输出控制。程序员可以根据需要将数据、结果、错误信息和日志信息分别输出到不同的流中,以便于用户查看、错误处理和调试。这种输出的分类和区分可以提高程序的可读性、可维护性和调试性。


预处理器

继承c的预处理器
需要特别说明的是头文件,c++的标准库中也兼容了c标准库,c的头文件形如name.h,在c++中则将这些文件命名为cname。从命名规范来说c++应当使用cname,在cname中定义的名字属于std,而.h中的则不是。
作用:可以避免命名冲突

类型

c++新基础类型

wchar_t,char16_t,char32_t。


Q:c++的wchar_t,char16_t,char32_t分别应用在哪些地方
chatgpt:
在C++中,wchar_t、char16_t和char32_t是用于表示不同字符编码的字符类型。它们的应用场景如下:

wchar_t:wchar_t是用于宽字符编码的数据类型,它可以存储更大范围的字符,包括Unicode字符集中的字符。通常用于处理多语言文字、国际化和本地化等需要支持广泛字符集的场景。在Windows平台上,wchar_t通常是16位的,使用UTF-16编码;而在其他平台上,wchar_t可以是32位的,使用UTF-32编码。

char16_t:char16_t是用于表示UTF-16编码的字符类型。UTF-16编码是一种用于表示Unicode字符的变长编码方式,它使用16位来表示大部分字符,但对于一些辅助字符(如表情符号)需要使用两个16位编码单元来表示。char16_t通常用于处理需要使用UTF-16编码的文本数据,例如在Windows平台上与系统API交互时。

char32_t:char32_t是用于表示UTF-32编码的字符类型。UTF-32编码采用固定的32位编码单元来表示所有的Unicode字符,因此每个字符都占用4个字节。char32_t通常用于处理需要使用UTF-32编码的文本数据,例如在某些需要精确处理Unicode字符的应用场景中。

需要注意的是,除非你明确需要处理特定的字符编码,否则在一般的C++编程中,使用标准的char类型就足够了。char类型通常用于表示ASCII字符集中的字符,它占用一个字节的存储空间。


左值引用和右值引用

引用是一种别名。
左值引用在有些方面和指针比较像,但也有区别,像const引用有临时量可以用于初始化,因此const int &r1=42;是合法的.而double dval=3.14;const int &ri = dval;也是合法的.

    const double val = 3.14;
	const double *pval = &val;//正确,指向一个double常量
	//const int    *pint = &val;//编译错误,指针类型不一致
	const int    &ri   = val;//正确,引用一个临时生成的int常量
	std::cout << "the addr of val and ri is "<<&val<<" "<<&ri<<std::endl;

	double dval = 3.14;
	const double *pdval = &dval;//正确,指向一个double变量
	//const int    *pdint = &dval;//编译错误,指针类型不一致
	//int    &rdi   = dval;//编译错误,非常量引用不能使用临时结果创建

	short foo = 1;
	short *pfoo = &foo;
	short &rfoo = *pfoo;
	std::cout << "the val of foo is "<<foo<<std::endl;
	rfoo =2;
	std::cout << "the val of foo is "<<foo<<std::endl;
	std::cout << "the addr of foo is "<<&foo<<std::endl;
	std::cout << "the addr of rfoo is "<<&rfoo<<std::endl;

输出为

the addr of val and ri is 0x7ffe7d6e84a0 0x7ffe7d6e849c
the val of foo is 1
the val of foo is 2
the addr of foo is 0x7ffd07f43cfa
the addr of rfoo is 0x7ffd07f43cfa

作用:函数传参时代替指针,增加可读性与简便化
和指针的区别:作为函数入参 ,数组名不会退化为指针
作为函数返回值,其他类型是右值,引用可作为左值。
右值引用即为必须绑定到右值的引用,右值是一个将亡值,即一个将要销毁的对象,因此才可以自由的将一个右值引用资源“移动”到另一个对象中。
不能将普通的左值引用绑定到一个右值,但const的左值引用可以绑定(实际额外生成了临时对象)。
不能将右值引用绑定到左值,因此也不能绑定到另一个右值引用类型的变量上(因为右值引用类型的变量是左值)。
std::move 将左值转换为对应的右值引用类型,不能使用using省略std,以避免命名冲突。


Q:c++中的左值引用和右值引用有什么用处?
chatgpt:
在C++中,左值引用(lvalue reference)和右值引用(rvalue reference)是引用类型的两种形式。它们的主要用途是进行对象的传递和绑定,以及支持移动语义和完美转发。

  1. 左值引用(lvalue reference):
    左值引用可以绑定到左值(具有名称的对象)上,例如变量、表达式结果或函数返回的左值。通过左值引用,可以修改绑定对象的值。左值引用常用于函数参数传递和赋值操作符的重载。

示例:

void increment(int& x) {
    x++;
}

int main() {
    int a = 5;
    increment(a);  // 通过左值引用修改a的值
    return 0;
}
  1. 右值引用(rvalue reference):
    右值引用可以绑定到右值(临时对象、字面量、表达式结果等)上。右值引用引入了移动语义,可以高效地转移资源的所有权,避免不必要的数据复制。右值引用还支持完美转发,可以保留传递参数的值类别(左值还是右值)。

示例:

class MyObject {
public:
    MyObject() {}
    MyObject(const MyObject& other) {
        // 拷贝构造函数
    }
    MyObject(MyObject&& other) {
        // 移动构造函数
    }
};

MyObject createObject() {
    MyObject obj;
    return obj;//关闭返回值优化, 这里会调用move ctor
}

int main() {
    MyObject a = createObject();  // 通过右值引用优化对象的移动构造,返回的是右值, 还会再次调用move ctor
    return 0;
}

通过右值引用,可以避免不必要的拷贝操作,提高性能。右值引用还为移动语义和完美转发提供了基础,使得在处理临时对象和转发函数调用时更加高效和灵活。
补充:上述最后一个例子可能需要添加编译参数-fno-elide-constructors才能实现

编译器会开启返回值优化,实际只会调用构造函数不会调用移动构造或移动赋值

$ g++ main.cpp -std=c++11 -o main
$ ./main
default constructor...
destructor...
$ g++ main.cpp -std=c++11 -o main -fno-elide-constructors
$ ./main
default constructor...
move constructor...
destructor...
move constructor...
destructor...
destructor...

Q:c++指针和引用的区别有哪些
chatgpt:
在C++中,指针和引用是两种不同的概念,它们有以下几个区别:

定义和初始化方式:指针是一个变量,可以通过使用*来声明一个指针类型,并使用&来获取变量的地址赋值给指针;引用是一个别名,可以通过使用&来声明一个引用类型,并在初始化时绑定到一个已存在的变量。

空值:指针可以被赋值为nullptr,表示它不指向任何有效的内存地址;引用必须在初始化时绑定到一个有效的对象,不能为null。

内存地址和操作:指针存储的是一个内存地址,可以通过解引用操作符*来访问指针指向的对象;引用本身并不存储内存地址,它只是一个别名,使用引用就相当于使用被引用的对象本身。

重新赋值:指针可以被重新赋值,可以指向不同的对象;引用一旦绑定到一个对象,就不能再重新绑定到其他对象。

空间占用:指针本身占用一定的内存空间,而引用不占用额外的内存空间。

函数参数传递:指针可以作为函数参数传递,可以通过指针修改传入的变量的值;引用也可以作为函数参数传递,通过引用修改传入的变量的值,但是在函数内部使用引用时,不需要使用解引用操作符*。

总的来说,指针提供了更灵活的内存操作能力,可以在运行时动态地改变指向的对象,但需要注意空指针和悬空指针的问题;引用提供了更简洁的语法和安全性,但是在使用时需要注意引用的生命周期和绑定对象的不可更改性。在实际使用中,根据具体的需求和场景选择适合的方式。


constexpr

constexpr变量在初始化时必须使用常量或常量表达式。对指针来说,const可以是顶层的(指针本身是常量),也可以是底层的(指向对象是常量),而constexpr就是顶层的。
作用:避免性能损失


Q:C++ contexpr的作用是什么
chatgpt:在C++中,constexpr是一个关键字,用于指示一个函数或变量可以在编译时求值。它的作用是让编译器在编译时进行常量表达式的计算,而不是在运行时进行。

使用constexpr可以将一些表达式标记为常量表达式,从而允许编译器在编译时对其进行求值,并将结果作为常量使用。这样可以在一定程度上提高程序的性能,因为编译时计算的结果可以在运行时直接使用,而不需要再进行重复的计算。

constexpr可以用于函数和变量的声明。对于函数,它表示函数可以在编译时进行求值,并且可以在编译时用于常量表达式的上下文中。对于变量,它表示变量可以在编译时进行初始化,并且可以用于常量表达式的上下文中。

使用constexpr时,需要注意以下几点:

  • constexpr函数必须满足一些限制条件,例如函数体必须是一个单一的返回语句,且只能包含一些简单的操作,如算术运算、逻辑运算和位运算等。
  • constexpr变量必须在声明时进行初始化,并且初始化表达式必须是一个常量表达式。
  • constexpr函数或变量可以在编译时求值,也可以在运行时求值,具体取决于使用的上下文和编译器的实现。

总的来说,constexpr的作用是允许在编译时进行常量表达式的求值,从而提高程序的性能并支持更灵活的编程。


auto

auto类型可通过初始值自动推断变量类型。也许应当应用于那些你知道是什么类型,但是懒的写的地方。
作用:自动推断类型

decltype

返回对应类型
作用:自动推断类型用与定义
总结:
1.作用于变量直接得到变量的类型;2.作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;3.作用与数组得到数组类型,不会自动转化为指针4.作用于函数名会得到函数类型,不会自动转换成指针


Q :c++ auto和decltype的作用和应用场景
chatgpt:
在C++中,autodecltype是用于类型推导的关键字。

auto关键字用于自动推导变量的类型。当使用auto声明变量时,编译器会根据变量的初始化表达式推导出变量的类型。例如:

auto x = 5;  // 推导出x的类型为int
auto y = 3.14;  // 推导出y的类型为double

auto关键字的应用场景包括:

  1. 简化类型声明:auto可以帮助简化复杂的类型声明,特别是在涉及模板和迭代器时。例如,使用auto可以简化迭代器的类型声明:
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    // 使用it进行迭代
}
  1. 函数返回类型推导:auto可以用于函数的返回类型,让编译器根据函数体中的返回语句推导出返回类型。这在涉及复杂的类型或模板返回类型时特别有用。
auto add(int a, int b) {
    return a + b;  // 推导出返回类型为int
}

decltype关键字用于获取表达式的类型。它返回表达式的静态类型,并且不会对表达式进行求值。例如:

int x = 5;
decltype(x) y;  // 推导出y的类型为int

double foo();
decltype(foo()) z;  // 推导出z的类型为double

decltype关键字的应用场景包括:

  1. 推导函数返回类型:decltype可以用于推导函数的返回类型,特别是在涉及复杂的表达式或模板返回类型时。
int x = 5;
double y = 3.14;

decltype(x + y) result;  // 推导出result的类型为double
  1. 推导模板类型:decltype可以与std::declval结合使用,用于推导模板类型。
template<typename T, typename U>
auto multiply(T t, U u) -> decltype(t * u) {
    return t * u;
}

在上述示例中,decltype(t * u)用于推导函数的返回类型,以确保返回值的类型与t * u的类型一致。

总而言之,autodecltype关键字在C++中用于类型推导,可以简化代码并处理复杂的类型。它们在简化类型声明、函数返回类型推导和模板类型推导等方面非常有用。


结构体

在C语言中结构体定义时候不能设置默认值(初始化),但在c++中可以。
在c++中的struct可以定义成员函数。


Q:c++中的结构体和类有哪些差异
chatgpt:
在C++中,结构体(struct)和类(class)有以下差异:

默认访问权限:在结构体中,默认的成员访问权限是公共的(public),而在类中,默认的成员访问权限是私有的(private)。

继承:类支持继承,可以通过派生类继承基类的成员。而结构体不支持继承。

默认构造函数:在类中,如果没有定义构造函数,编译器会自动生成默认构造函数。而在结构体中,如果没有定义构造函数,编译器也会生成默认构造函数。

类型的使用习惯:一般来说,结构体用于表示一组相关的数据,而类用于表示一组相关的数据和操作。

虽然结构体和类在语法上有一些差异,但在C++中,结构体和类的本质是相同的,都可以用来封装数据和行为,只是默认的访问权限和一些语法特性有所不同。


类型转换

隐式转换
显式转换
c++新增了类型转换指示符:static_cast,dynamic_cat,const_cast,reinterpret_cast.
作用:相对于c风格强制类型转换,其表现形式更加明了,便于代码review和问题追踪。

别名

using以及c风格typedef

变量

花括号初始化变量的全面应用。int a ={0};
空指针赋值:nullptr

运算符

在c基础上扩充,比如::,typeid,显式转换cast,new,delete,new [],delete[],noexcept,throw等。

表达式

运算的最基础单元,基本与c一致。

语句

和c基本一致。增加内容如下:
throw,try,catch语句块。

控制流

顺序,选择,循环,和c基本一致。范围for循环语句可用于迭代。

函数

函数重载

形参列表不同的同名函数。

默认实参

c++支持函数的默认实参,为了调用时不造成二义性,如果一个参数被赋予了默认值,那么后面所有的参数都要有默认值。甚至可以添加默认实参,可以将表达式用来初始化默认实参,但是局部变量不可做为默认实参。

函数返回多个值

return {a,b,c};

其本质类似于列表初始化了返回值。

尾置返回类型

int (*func(int i))[10];
auto func(int i)->int(*)[10];

以上两个句子都定义了一个函数,这个函数接受int作为入参,返回值是一个包含10个int的一维数组指针。第二种方式就是使用了尾置返回类型。第一种真的是太绕了啊啊啊啊啊啊啊。

constexpr函数

默认为内联函数,其返回值为constexpr类型,constexpr类型函数是指能够用于常量表达式的函数。 需要注意的是,constexprt函数返回的不一定是常量表达式。

c/c++混合编程

c和c++可以混合编程,只要将函数正确声明即可相互调用:

#ifdef __cplusplus
extern "C"{
#endif

	//函数声明

#ifdef __cplusplus
}
#endif

这是因为同一个函数在c和c++编译时对应的符号不同,c++支持重载,可以有多个同名函数,编译时的符号是原函数名加前后缀,根据调用时的传参来选择调用对应的函数,因此如果直接用c函数调用c++代码,链接时会报符号未定义。extern "C"表示向C兼容,不要使用函数重载机制,extern “C”{}是c++的机制,在C中是没有extern “C”{}这个用法的,C语言使用编译会报错,因此有条件编译宏。如果用了extern “C”{},还想用重载的函数,那只能给每个重载的函数套壳取不同的名字了。
同样的,当c++代码调用c代码时,c代码也需要使用extern “C”{},以告诉编译器,不要使用按照函数重载机制去调用函数。

this指针

声明成员函数为const,即在函数参数列表后加入const,通过这种方式可以修改隐式this指针,使其为底层const,而this指针默认为顶层const。

std::string isbn() const {return bookNo;}

像这样声明的函数叫做常量成员函数。
this指针可用于返回对象的引用

FOO& FOO::returnSelf()
{
	//
	return *this;
}

构造函数和析构函数

构造函数不能被声明成const的,只有当构造函数完成初始化过程,常量的对象才能真正的取得其常量属性。
当未指定构造函数时,编译器提供合成的默认构造参数,默认构造参数将遵循以下规则:
1.如果存在类内初始值,将用他来初始化成员
2.否则,默认初始化该成员
需要注意的是,如果有了其他构造函数,编译器就不会自动生成默认构造函数了,可以使用=default显式的使用默认构造函数:

Class FOO{
FOO()=default;
FOO(int a);
}

另外,如果一个构造函数为所有参数都指定了默认实参,那么他实际上也定义了默认构造函数。

Class FOO{
FOO(std::string s=""):nums(s){};
std::string nums;
}

析构函数不接受参数,不能被重载,对于给定类,只会有唯一一个析构函数。

构造函数初始值列表

Class FOO{
FOO(int a):one(a),two(a) {/*do some thing*/};
int one;
int two;
}

函数参数列表后跟冒号,附带初始值列表,以逗号分隔。以上代码意思是用a来初始化one和two这两个成员变量。
需要注意的是,有些时候必须使用构造函数初始值列表。比如成员是const,引用或无默认构造函数的类类型,而不是在构造函数的函数体内赋值。
另外构造函数初始值列表执行顺序是不定的,而成员初始化顺序与他们定义的顺序一致,因此最好让列表的顺序和成员声明的顺序一致。

拷贝构造和移动构造

直接初始化:使用普通的函数匹配
拷贝初始化:将右侧的对象拷贝到正在创建的对象中,如果需要的话可能还要进行类型转换。
拷贝初始化是依靠拷贝构造函数和移动构造函数完成的。
拷贝初始化发生的时机:
1.使用=定义变量
2.将一个对象作为实参传递给一个非引用类型的形参
3.将一个返回类型为非引用类型的函数返回一个对象
4.用花括号列表初始化一个数组中的元素或一个聚合类中的成员

拷贝构造函数:第一个参数是自身的左值引用,且任何额外的参数都有默认值。
移动构造函数:第一个参数是自身的右值引用,且任何额外的参数都有默认值。
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。


Q:c++发生拷贝构造和移动构造的时机分别有哪些,请举例说明
chatgpt:
在C++中,拷贝构造和移动构造函数都是用于对象的复制或移动操作。它们的时机和用途略有不同。

  1. 拷贝构造函数的时机:

    • 当使用一个对象初始化另一个对象时,会调用拷贝构造函数。例如:
      MyClass obj1;  // 创建一个对象
      MyClass obj2(obj1);  // 使用obj1初始化obj2,调用拷贝构造函数
      
    • 当对象作为函数参数按值传递时,会调用拷贝构造函数。例如:
      void func(MyClass obj);  // 参数obj按值传递,调用拷贝构造函数
      
    • 当函数返回一个对象时,会调用拷贝构造函数。例如:
      MyClass func() {
          MyClass obj;
          return obj;  // 返回一个对象,调用拷贝构造函数
      }
      
  2. 移动构造函数的时机:

    • 当使用一个临时对象(右值)初始化另一个对象时,会调用移动构造函数。例如:
      MyClass obj1;  // 创建一个对象
      MyClass obj2(std::move(obj1));  // 使用obj1的资源初始化obj2,调用移动构造函数
      
    • 当对象作为函数返回值时,并且返回的对象是一个临时对象(右值)时,会调用移动构造函数。例如:
      MyClass func() {
          MyClass obj;
          return std::move(obj);  // 返回一个临时对象,调用移动构造函数
      }
      
    • 当对象被转换为右值引用时,会调用移动构造函数。例如:
      MyClass obj1;  // 创建一个对象
      MyClass obj2 = std::move(obj1);  // obj1被转换为右值引用,调用移动构造函数
      

需要注意的是,C++11引入了右值引用和移动语义,通过移动构造函数可以避免不必要的拷贝操作,提高性能。移动构造函数通常会将资源所有权从一个对象转移到另一个对象,而不是创建资源的副本。


Q:
chatgpt:
在 C++ 中,可以定义多种类型的构造函数。以下是几种常见的构造函数类型:

  1. 默认构造函数(Default Constructor):没有参数的构造函数,用于创建对象时不需要提供任何初始值。
class MyClass {
public:
    MyClass() {
        // 构造函数的实现
    }
};
  1. 带参数的构造函数(Parameterized Constructor):接受一个或多个参数,用于在创建对象时提供初始值。
class MyClass {
public:
    MyClass(int value) {
        // 构造函数的实现
    }
};
  1. 拷贝构造函数(Copy Constructor):接受同一类对象的引用作为参数,用于创建一个新对象,并将参数对象的值复制给新对象。
class MyClass {
public:
    MyClass(const MyClass& other) {
        // 构造函数的实现
    }
};
  1. 移动构造函数(Move Constructor):接受同一类对象的右值引用作为参数,用于创建一个新对象,并从参数对象中“移动”资源或状态。
class MyClass {
public:
    MyClass(MyClass&& other) {
        // 构造函数的实现
    }
};
  1. 复制赋值运算符(Copy Assignment Operator):用于将一个对象的值复制给另一个已存在的对象。
class MyClass {
public:
    MyClass& operator=(const MyClass& other) {
        // 赋值运算符的实现
        return *this;
    }
};

这些是常见的构造函数类型,你可以根据需要在类中定义它们。注意,以上只是示例,实际的构造函数实现可能会根据类的具体需求而有所不同。


Q:这几种构造函数分别怎么使用,请举例?
当定义了不同类型的构造函数后,可以根据需要使用它们来创建对象。以下是每种构造函数的使用方法和示例:

  1. 默认构造函数:使用无参数的构造函数创建对象,不需要提供任何初始值。
MyClass obj;  // 使用默认构造函数创建对象
  1. 带参数的构造函数:使用提供的参数值来初始化对象的成员变量。
MyClass obj(10);  // 使用带参数的构造函数创建对象,并将参数值传递给构造函数
  1. 拷贝构造函数:使用同一类对象的引用作为参数,创建一个新对象,并将参数对象的值复制给新对象。
MyClass obj1(10);       // 创建第一个对象
MyClass obj2(obj1);    // 使用拷贝构造函数创建第二个对象,将第一个对象的值复制给第二个对象
  1. 移动构造函数:使用同一类对象的右值引用作为参数,创建一个新对象,并从参数对象中“移动”资源或状态。
MyClass CreateObject() {
    MyClass obj(10);
    return obj;
}

MyClass obj1 = CreateObject();  // 创建第一个对象
MyClass obj2(std::move(obj1)); // 使用移动构造函数创建第二个对象,并从第一个对象中移动资源或状态
  1. 复制赋值运算符:使用赋值运算符将一个对象的值复制给另一个已存在的对象。
MyClass obj1(10);   // 创建第一个对象
MyClass obj2;       // 创建第二个对象

obj2 = obj1;        // 使用复制赋值运算符将第一个对象的值复制给第二个对象

以上是每种构造函数的简单示例,你可以根据具体的需求和类定义来选择使用哪种构造函数。注意,示例中的类名为 “MyClass”,在实际使用时,需要根据自己的类名进行替换。


运算符重载

运算符重载本质上是函数,以赋值运算符为例,赋值运算符本质上是一个名为operator=的函数

class Foo{
public:
    Foo(std::string str=std::string()):s(str){
    std::cout<<"direct construct"<<endl;
    };
    Foo(const Foo& f);
    Foo &operator=(const Foo& f);
    Foo(Foo&& f) noexcept;
    Foo &operator=(Foo&& f) noexcept;
    std::string s;
};
Foo::Foo(const Foo& f){
    std::cout<<"copy construct"<<endl;
    this->s=std::string(f.s);
}
Foo &Foo::operator=(const Foo &f){
    std::cout<<"= copy operator"<<endl;
    this->s=std::string(f.s);
    return *this;
}
Foo::Foo(Foo&& f) noexcept{
    std::cout<<"move construct"<<endl;
    this->s=f.s;
}
Foo &Foo::operator=(Foo &&f) noexcept{
    std::cout<<"= move operator"<<endl;
    if(&f != this){
        this->s=f.s;
        return *this;
    }
    return *this;
}
int main(){
    Foo foo("abc");
    std::cout<<"foo="<<foo.s<<endl;
    Foo foo2 = foo;
    std::cout<<"foo2="<<foo2.s<<endl;
    Foo foo3;
    foo3=foo2;
    std::cout<<"foo3="<<foo3.s<<endl;
    foo3=std::move(foo);
    std::cout<<"foo3="<<foo3.s<<endl;
}

输出如下:

direct construct
foo=abc
copy construct
foo2=abc
direct construct
= copy operator
foo3=abc
= move operator
foo3=abc

在这里看出,赋值运算符是在向定义好的对象赋值时发生的,并不是用了=就会调用。拷贝构造函数和移动构造函数调用于对象构造时。

引用限定符

用来指出一个非static成员函数可以用于左值或右值。限定符&或&&应该放在参数列表或const后面。

class Foo{
	public:
	Foo func() &;//可用于任何类型的Foo
	Foo func2() &&;//可用于可改变的右值
};

委托构造函数

使用其所属类的其他构造函数执行他自己的初始化

Class FOO{
FOO(int a);
FOO():FOO(3){/*do sth*/};
}

隐式的类类型转换

如果构造函数只接受一个实参,则他实际上定义了转换为此类类型的隐式转换机制,这种构造函数被称为转换构造函数。
如下代码合法:

class FOO{
public:
	FOO(int num):s(num){};
	const FOO& returnFoo(const FOO &f){return f;};
	int s;
};
int main(){
    FOO ff(2);
    FOO fff(3);
    auto ffff = fff.returnFoo(1);
    cout<<ffff.s<<endl;
}

最后的输出是1.在这里,int被隐式的转换为FOO类型了。需要注意的是,上述转换只能进行一次。
比较常用的隐式转换可能就是string类.
可以使用explicit阻止隐式转换。

class FOO{
public:
	explicit FOO(int num):s(num){};
	const FOO& returnFoo(const FOO &f){return f;};
	int s;
};

当使用explicit后,只能以直接初始化的形式使用,不能拷贝初始化(使用=)

访问说明符

public,protect,private
友元类,友元函数

可变数据成员mutable

mutable数据成员可以在const成员函数内修改,mutable成员永远不会是const。

依据对象的常量类型重载成员函数

 Class FOO{
FOO &display(){/*do sth*/return *this}
const FOO &display() const {/*do sth*/return *this}
}

上述两个重载的函数可以在对象是const时返回const引用,对象非const时,返回普通引用。

类的静态成员

类的变量和成员函数可以定义为static,类的静态成员存在于任何对象之外,而是与类本身关联。因此静态成员函数没有this指针,也不能被声明为const函数。
static关键子只能出现在类内部的声明语句中。

类的作用域

在外部定义类的成员函数时,必须显式的指定类名。

class FOO{
int clear();
}
int FOO::clear()
{
	/*do sth*/
}

在外部时,类的作用域开始于类名处,因此若函数返回值使用了类的数据类型,则需要显式添加:

class FOO{
typedef myint int;
myint clear();
}
FOO::myint FOO::clear()
{
	/*do sth*/
}

更进一步的,编译器是在处理完类的所有声明后才处理成员函数的定义的。

定义删除的函数

输出的函数通过=delete定义,虽然声明了删除的函数,但不能以任何形式使用他们,从而达到阻止特定操作的目的。
任何函数都可以指定delete.

stl标准库

IO类

iostream,fstream,sstream分别对应流,文件和string
条件状态:所有流都可使用的指示流状态的标志和函数

顺序容器

array

大小固定,支持快速随机访问,不支持增加删除元素

forward_list

单向链表,只支持单向顺序访问,任何位置插入删除都很快。

list

双向链表,只支持双向顺序访问,任何位置插入删除都很快。

deque

双端队列,支持随机访问(连续内存),在头尾插入删除很快。

string

与vector类似,但专门保存字符。
string与c风格字符串:

   string s = "hello";
    const char *pstr = s.c_str();
    cout << &s << " " << (void*)pstr << endl;
    cout << (int)(s[5])<<endl;
    cout << (int)(pstr[5])<<endl;

输出:

0x16f90b1e8 0x16f90b1e8
0
0
vector

可变大小数组(可能存在内存重分配) 支持快速随机访问(连续内存) 除尾部外插入删除很慢(连续内存)

关联容器

关联容器的值不能改变

set

有序,不重复

map

有序,不重复

multimap

有序,可重复,相同关键字的元素相邻存储

multiset

有序,可重复,相同关键字的元素相邻存储

unordered_map

无序,使用hash散列到哈希桶,不重复

unordered_set

无序,使用hash散列到哈希桶,不重复

unordered_multimap

无序,使用hash散列到哈希桶,相同关键字的元素相邻存储

unordered_multiset

无序,使用hash散列到哈希桶,相同关键字的元素相邻存储

map的下标操作

可以使用下标,用键来做索引

initializer_list

用于编写参数个数可变但类型一样的函数。
initializer_list对象中的元素永远是常量。

适配器

接收一个标准库类型,函数或迭代器的标准库类型,函数或迭代器,使其行为像另一个类型,函数或迭代器一样。

顺序容器适配器
stack
queue
priority_queue

迭代

迭代器

有迭代器的类型都会有返回迭代器的成员函数:begin end,比如

auto b = v.begin(),e=v.end();

b为第一个元素的迭代器,e为尾元素的下一个位置的迭代器,只是个标记。
另外还有两个成员cbegin和cend,返回的迭代器为const_iterator,来应对只需读无需写的场景。
数组也可以使用begin和end,但需要作为参数:

int ia[]={0,1,2};
int *beg = begin(ia);
int *last = end(ia); 
迭代器分类
插入迭代器
流迭代器
反向迭代器
移动迭代器
迭代器按操作分类
输入迭代器
输出迭代器
前向迭代器
双向迭代器
随机访问迭代器
iostream迭代器

istream_iterator
ostream_iterator

基于范围的 for 循环

应当是迭代器的一种值传递。

 std::vector<int> arr = { 1, 2, 3 };
    for(auto n:arr)
    {
        std::cout<<"the addr of n and arr is "<< &n << " "<< &arr[0] << endl;
        std::cout<<"the type of nn and arr is "<< typeid(n).name() << " "<< typeid(arr).name() << endl;
    }
    cout<< "----"<<endl;
    for(auto &nn:arr)
    {
        std::cout<<"the addr of nn and arr is "<< &nn << " "<< &arr[0] << endl;
        std::cout<<"the type of nn and arr is "<< typeid(nn).name() << " "<< typeid(arr).name() << endl;
    }

打印如下:

he addr of n and arr is 0x16f71b194 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE
the addr of n and arr is 0x16f71b194 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE
the addr of n and arr is 0x16f71b194 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE
----
the addr of nn and arr is 0x6000035e8040 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE
the addr of nn and arr is 0x6000035e8044 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE
the addr of nn and arr is 0x6000035e8048 0x6000035e8040
the type of nn and arr is i NSt3__16vectorIiNS_9allocatorIiEEEE

可以看到地址的不同,可能是栈变量和堆变量的区别。
需要注意,数组也可以使用范围for语句,额外的,多维数组同样可以,但是除最内层循环外,其他外层循环均需引用,否则将退化为指针。

泛型算法

算法永远不会改变底层容器的大小。若要达到插入操作,可以使用插入器。

排序

稳定的排序函数可以在已排序的基础上排序
sort
stable_sort

查找

find
findif

向算法传递函数

可调用对象有四种:函数,函数指针,lambda表达式和重载了函数调用运算符的类。

谓词

谓词是一个可调用的表达式,其返回结果能用来当作算法的条件,返回的是bool类型值。

lambda表达式

lambda表达式是一个匿名函数,在需要一元谓词而需要至少两个参数时,可以使用lambda表达式。
其形式如下:
[捕获列表] (参数列表) ->返回类型 {函数体}
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和他所在函数之外声明的名字。
捕获列表可以显式捕获(指定变量名)或隐式捕获(仅指定捕获方式),&与=分别代表传引用和传值

可变lambda

使用mutable声明:
[capture_list] (params_list) mutable exception -> return type { function body }

重载函数调用运算符的类
使用函数适配器bind来生成新的可调用对象

bind可以将一个或多个参数绑定到一个可调用表达式。
bind可以使用占位符来重排参数顺序
lambda可以捕获引用,而bind适配器若要传递参数的引用,可以使用ref()。
cref是另一个版本,他返回const引用。

算法形参模式

4种普遍形式:

alg(beg,end,other args);
alg(beg,end,dest,other args);
alg(beg,end,beg2,other args);
alg(beg,end,beg2,end2,other args);
算法命名
重载形式传递谓词
unique(beg,end);
unique(beg,end,comp);
_if版本算法
find(beg,end,val);
find_if(beg,end,pred);
_copy版本算法
reverse(beg,end);
reverse_copy(beg,end,dest);

智能指针

shared_ptr

带引用计数的指针,允许多个指针指向同一个对象.当指向对象的最后一个shared_ptr被销毁,shared_ptr类将会调用析构函数自动销毁此对象。

make_shared

动态分配对象并初始化,返回指向此对象的shared_ptr.

和new结合使用

必须使用直接初始化,不支持拷贝或赋值:

shared_ptr<int> p1(new int(1024));

不推荐两者混用

unique_ptr

独占所指向的对象
必须使用直接初始化,不支持拷贝或赋值:

unique_ptr<int> p1(new int(1024));

release() 放弃并返回资源的指针
reset() 释放资源指针

weak_ptr

弱引用 ,指向shared_ptr所管理的对象
将weak_prt绑定到一个share_ptr不会改变shared_ptr的引用计数。
作用:weak_ptr的不会影响一个对象的生命周期。

智能指针与删除器

若智能指针管理的资源不是new分配的内存,则需要使用删除器来正确释放资源。

动态数组
分配

使用new []
int *pia=new int[get_size()];

销毁

使用delete []
元素会从最后一个开始,倒着往前销毁。
需要注意的是,若想用share_ptr管理动态数组,必须提供自定义的删除器,并且shared_ptr是不直接支持管理动态数组的,实际模板参数中是T而不是T [],这是unique_ptr是不同的。

allocator类

allocator可以实现将内存的分配和对象的构造分离。
使用allocator一般需要遵循以下步骤:
allocator <T> a 创建allocator类的对象a
p = a.allocate(n) 分配未构造的内存
a.construct(p,args) args为构造函数,在p所指向的内存中构造一个对象
a.destroy§ 对p指向的对象执行析构函数
a.deallocate(p,n) 释放p指向的内存


Q:c++中使用new []动态分配的数组与使用maclloc分配的内存有什么不同
chatgpt:
在C++中,使用new []运算符动态分配数组与使用malloc函数分配内存有几个重要的不同之处:

  1. 类型安全性:new []运算符会根据所请求的类型进行内存分配,而且会调用相应类型的构造函数来初始化数组元素。这意味着使用new []分配的数组是类型安全的,并且可以直接使用数组元素的成员函数和操作符。相比之下,malloc函数只返回一个void*指针,需要手动进行类型转换,并且不会调用构造函数初始化内存。

  2. 内存大小:new []运算符在分配数组时,需要指定元素的个数,它会根据元素的大小和个数计算所需的总内存大小。而malloc函数需要手动计算所需的内存大小,并将其作为参数传递给函数。

  3. 内存对齐:new []运算符会根据所请求的类型进行内存对齐,以确保数组元素按照正确的字节边界存储。而malloc函数不会进行内存对齐,可能导致性能下降或者访问错误。

  4. 构造和析构函数的调用:使用new []运算符分配的数组会自动调用每个元素的构造函数进行初始化,并在数组释放时调用每个元素的析构函数进行清理。而malloc函数只是简单地分配一块内存,并不会调用构造和析构函数。

  5. 内存的释放:使用new []运算符分配的数组应该使用delete []运算符进行释放,而malloc函数分配的内存应该使用free函数进行释放。

总的来说,使用new []运算符分配数组更符合C++的面向对象特性,提供了更高的类型安全性和方便性,而malloc函数则更接近C语言的内存分配方式,更加灵活但需要手动管理内存的生命周期。在C++中,推荐使用new []delete []来分配和释放动态数组。


待复习

const 引用 别名 指针 auto decltype typedef constexpr
类的作用域
迭代器
初始化方式 直接初始化 拷贝 赋值 初始化器

参考文章

C++Primer 设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
c++ 11 for循环
C++的移动构造和移动赋值运算符
详解C++移动语义std::move()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值