C++语法小结
1.for auto用法
for(auto iter:vec)不改变迭代对象的值,for(auto &iter:vec)可以改变迭代对象的值。
两者都可以获取到迭代容器中的值,但是使用auto iter时不会对容器对象造成改变,而使用auto &iter,对于iter的任何修改将直接修改容器内对应的值。
2.关于vscode调试C++代码的问题
由于vscode编译模式的问题,要使用debug模式,要保证文件名称是英文/或放在debug.cpp里。
3.关于Cin, getline 和 get的区别
常规cin读取一个单词(按空白[空格、制表符和换行符]来确定字符串结束的位置)
cin.getline()和cin.get()都读取一行输入,直到换行符。
区别是getline()将丢弃换行符,而get()将换行符保留在输入序列中。
4.关于Fibonacci数列的时空复杂度问题
Fibonacci | T(n) | S(n) |
---|---|---|
非递归 | O(n) | O(1) |
递归 | O(2^n) | O(n) |
递归优化 | O(n) | O(n) |
求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。
Fibonacci非递归:
int fibonacci(int n){
if(n <= 0){
return 0;
}else if(n == 1){
return 1;
}else{
int first = 0;
int second = 1;
int res = 1;
for(int i = 2; i <= n; i++){
res = first + second;
first = second;
second = res;
}
return res;
}
}
Fibonacci递归:
int fibonacci(int n){
if(n <= 0){
return 0;
}else if(n < 3){
return 1;
}else{
return fibonacci(n-1) + fibonacci(n-2);
}
}
Fibonacci递归优化:
int fibonacci(int first, int second, int n){//first 和second初始化均为1(作为参数首次传入)
if(n <= 0){
return 0;
}else if(n < 3){
return 1;
}else if(n == 3){
return first + second;
}else{
return fibonacci(second, first + second, n-1);
}
}
5.关于结构体变量和结构体指针访问成员的问题
对于结构struct,何时使用句点运算符,何时使用箭头运算符:如果结构标识符是 结构名,使用句点运算符one.price;如果标识符是指向结构的指针,则使用箭头运算符ps->price,或者使用(*ps).price。
6.关于结构数组的指针成员使用方法
onestruct arr0,arr1,arr2;
const onestruct * arp[3] = {&arr0,&arr1,&arr2};
1. arp[0/1/2]->
2. const onestruct ** ppa = arp; (*(ppa+1))->
3. auto ppb = arp; (*(ppb+1))->
7.vector 与 array ——变长数组与固定长度数组
vector:
#include<vector>
vector<typename> vi;
vector<typename> vt(n_elem);
n_elem可以是整型常量,也可以是变量
array:
#include<array>
array<typename, n_elem> arr;
n_elem不能是变量
- 无论是数组、vector对象还是array对象,都可以用标准数组表示法来访问各个元素;
- array对象和数组存储在相同的内存区域(栈)中,而vector对象存储在另一个区域(自由存储区或堆)中;
- 可以将一个array对象赋给另一个array对象,而数组只能逐元素复制。
8.关于类定义前后缀运算符的效率
前缀版本++x:将值加1,然后返回结果
后缀版本x++:先复制一个副本,将其加一,然后将复制的副本返回
因此,对于类而言,前缀版本的效率高于后缀版本。
对于内置类型,采用哪种格式不会有差别;但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高。
9.关于逗号运算符的作用
- 将两个或更多的表达式放到一个for循环表达式中
- 确保先计算第一个表达式,然后计算第二个表达式(即,逗号运算符是一个顺序点)
- 逗号表达式的值为最后一个逗号后的值
- 逗号运算符在所有运算符中的优先级是最低的:即 cats = 17,240; 被解释为 (cats = 17),240; 但是当 cats = (17,240); cats被赋值为240
10.关于vector构造多维数组
vector <char> hash1[7][7];//7*7矩阵,矩阵元素为存储char类型的vector
hash1[0][0].push_back(0);//right, element is the vector(can be expand).
hash1[0]->push_back('t');//right
hash1[0]->push_back('temp');//wrong,just input 'p'
vector<vector<char>> hash2(7,vector<char>(7)) ;//7*7矩阵,矩阵元素为char
hash2[0].push_back('a');//right,only hash2[0] will expand, other e.g.hash2[1/...] won't expand
hash2[0][0].push_back(0);//wrong, element is the char type not the vector(can't be expand).
11.关于函数传入数组(是否修改原数组内容的声明)
- 若函数要修改数组 void f_modify(double ar[], int n);
- 若函数不修改数组 void f_nochange(const double ar[], int n); (传入的数组可以不是const常量类型)
- 函数不能使用sizeof来获悉原始数组的长度(sizeof(ar)返回地址的长度),而必须依赖于程序员传入正确的元素数。
- const数据和非const数据都可以赋给const指针,但是const数据不能赋给非const指针;且当且仅当只有一层间接关系(指针指向基本数据类型时),才可以将非const地址赋给const指针。即const只能用于指向基本类型的的指针(一维数组名),而不能指向指针的指针(二维数组名)
int age = 39;
const int * pt = &age; // 不能使用*pt=?修改pt指向的值,但可以使用age=?修改
int sage = 80;
pt = &sage; //但可以将一个新地址赋值给pt,这表示*pt属于常量const,而pt不是
int * const finger = &age; //finger和pt是常量,只能为age地址,不能赋给它新的地址,但可以修改*finger=?(因为*finger和age不是常量)
12.关于vector 迭代器用法
vector<int> nums = {-4,-1,0,3,10};
for(auto item:nums)
cout <<item<<',';
等同于
for(vector<int>::iterator it=nums.begin();it <nums.end();it++)
cout <<*it;
13.引用 &
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
int a = 0;
int b = 1;
int &c = a; //c指向a所属元素
a 0 b 1 c 0
&a 0x61fe0c &b 0x61fe1c &c 0x61fe0c
c = b; //此时实质上为给a赋值1,c和a的地址不变
a 1 b 1 c 1
&a 0x61fe0c &b 0x61fe1c &c 0x61fe0c
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
尽可能使用const声明引用变量的理由:
- 使用const可以避免无意中修改数据的编程错误;
- 使用const使函数能够处理const和非const实参,否则只能接受非const数据;
- 使用const引用使函数能够正确生成并使用临时变量。
14.内联 inline
和常规函数调用相比,程序无需跳到另一个位置执行代码后再跳回来,运行速度比常规函数稍快,但需要占用更多内存。
应有选择地使用内联函数,适用于:函数代码执行时间短,且经常被调用,则内联调用就可以节省非内联调用使用的大部分时间;不适用于:执行函数代码的时间比处理函数调用机制的时间长,则节省的时间只占整个过程的很小一部分。
在函数声明和函数定义前都需加上关键字 inline (通常做法是省略原型,将整个定义[函数头+所有函数代码]放在本应该提供原型的地方)
程序员请求将函数作为内联函数时,编译器不一定会满足要求:1.函数过大2.内联函数不能递归
内联与C语言宏#define(内联代码的原始实现)的区别:宏不是通过传递参数实现的,而是通过文本替换实现的,即宏不能按值传递
#define SQUARE(X) X*X
a = SQUARE(5.0); is replaced with by a = 5.0*5.0; true
b = SQUARE(4.5 + 7.5); is replaced with by b = 4.5 + 7.5 * 4.5 + 7.5; wrong
c = SQUARE(c++); is replaced with by c = c++ * c++; wrong c自增两次
15.函数重载/多态
- 可以通过定义同名函数但特征标(参数列表)不同来实现服务不同的数据类型。
16.函数模板
- 自动完成重载函数的过程,使用泛型和具体算法来定义函数
- 函数声明:
template <typename T> 或 template <class T>
void func(T (&)a);
- 函数定义:
template <typename T> 或 template <class T>
void func(T (&a)){
...
}
- 重载的函数模板:(并非所有的模板参数都必须是模板参数类型T)
template <typename T> 或 template <class T>
void func(T (&)a);
template <typename T> 或 template <class T>
void func(T (&)a, int n);
int main(){
...
}
template <typename T> 或 template <class T>
void func(T (&)a){
...
}
template <typename T> 或 template <class T>
void func(T (&)a, int n){
...
}
17.栈和队列
- 栈和队列在C++中常用SGI STL,默认是以deque为缺省情况下的底层结构,
- 两者都是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能),如:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
stack<type> s; 定义一个参数类型为 type 的栈
s.push() 压栈,无返回值
s.emplace() 压栈,无返回值(与push的区别下面细说)
s.pop() 栈顶元素出栈,不返回元素,无返回值
s.top() 返回栈顶元素,该元素不出栈
s.empty() 判断栈是否为空,是返回 true
s.size() 返回栈中元素数量
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
push() 在队尾插入一个元素
emplace() 在队尾插入一个用传给emplace()的参数构造的元素
pop() 删除队列第一个元素
size() 返回队列中元素个数
empty() 如果队列空则返回true
front() 返回队列中的第一个元素
back() 返回队列中最后一个元素
- 因此两者都往往不被归类为容器,而被归类为container adapter( 容器适配器)
- 栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
- 队列为先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
- 两者的pop返回值都为void,不返回移除元素,要返回使用top(),但top不移出只返回。
- 栈的push和emplace操作区别:
- 首先 s.push() 与 s.emplace() 的最终执行效果是一模一样的,都是在栈顶加入一个元素,差别就是压栈元素的来源可能不同;
- push() 接受一个已经存在的元素(比如已实例化的类或变量),并将它的副本附加到容器中。push总是只接受一个参数,即要复制到栈顶中的元素;
- emplace() 可以现场通过参数列表创建该类的一个实例放到栈顶。要放置的参数将作为参数转发给栈中所含的类的构造函数。如果类有默认构造函数,emplace 可以有一个参数、多个参数,或者根本没有参数。
- 例如,当栈的参数是类时,push的参数必须是已实例化的类名作为参数,而 emplace 则可以直接使用类初始化参数现场初始化一个类实例加入栈顶。因此 emplace 的功能比 push 更强大,且兼容 push,但一般使用过程中,使用 push 就足够了。
18.struct和class关于构造函数的区别(https://blog.csdn.net/alidada_blog/article/details/83419757)
- 在C中,因为struct是一种数据类型,那么就肯定不能定义函数,所以在面向c的过程中,struct不能包含任何函数。否则编译器会报错。
- 面向过程的编程认为,数据和数据操作是分开的。然而当struct进入面向对象的c++时,其特性也有了新发展,就拿上面的错误函数来说,在c++中就能运行,因为在c++中认为数据和数据对象是一个整体,不应该分开,这就是struct在c和c++两个时代的差别。
在C++中struct得到了很大的扩充:
1.struct可以包括成员函数
2.struct可以实现继承
3.struct可以实现多态 - strcut和class的区别
- 1.默认的继承访问权。class默认的是private,strcut默认的是public。
- 2.到底默认是public继承还是private继承,取决于子类而不是基类。我struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。
- 3.默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
- 4.“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。
- 从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
- 5.class和struct在使用大括号{ }初始化的区别
- class和struct如果定义了构造函数的话,都不能用大括号进行初始化
- 如果没有定义构造函数,struct可以用大括号初始化。
- 如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化。
- 两者最大的区别就在于思想上,c语言编程单位是函数,语句是程序的基本单元。而C++语言的编程单位是类。从c到c++的设计由以过程设计为中心向以数据组织为中心转移。
类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是:结构的默认访问类型是public,而类的默认访问类型是private。
19.二叉搜索树
- 遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。
20.map排序
- 对有序map中的key排序
- 如果在有序的map中,key是int,或者string,它们天然就能比较大小,本身的就是有序的。不用额外的操作。
-可以自定义,按照键值升序排列,注意加载
// #include <functional> // std::greater // map<string, int, greater<string>> name_score_map;
- 如果在有序的map中,key是int,或者string,它们天然就能比较大小,本身的就是有序的。不用额外的操作。
- 对有序map中的value排序
- 把map中的元素放到序列容器(如vector)中,再用sort进行排序
21.二叉树,完全二叉树,二叉搜索树,平衡二叉树,平衡二叉搜索树,堆之间的异同
- 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?
- 是的,是二叉搜索树和平衡二叉树的结合。
- 平衡二叉树与完全二叉树的区别在于底层节点的位置?
- 是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
- 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
- 堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。
22.成员函数和友元函数的区别
程序数据: 数据是程序的信息,会受到程序函数的影响。封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。
数据封装引申出了另一个重要的 OOP 概念,即 数据隐藏 。数据封装 是一种把数据和操作数据的函数捆绑在一起的机制, 数据抽象 是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。
由于C++的封装和隐藏特性,只有类定义的成员函数可以访问类定义的私有数据。
成员函数是数据封装和数据隐藏的机制。
友元是C++提供的一种破坏数据封装和数据隐藏的机制。
成员函数
class Stock{ //class declaration
private:
std:: string company;
long shares;
double share_val;
double total_val;
void set_tot(){ total_val=shares* share_val;}
public:
void acquire(const std:: string & co, long n, double pr);
void buy(long num, double price);
void se11(long num, double price);
void update(double price);
void show();
};//note semicolon at the end
注意这里面的private可以不写,如果不写的话默认是私有的。
其中,company、shares等都是Stock类的私有数据成员。如果试图使用非成员函数访问这些数据成员编译器禁止这样做。如果试图破解该机制,友元是另一种选择。
实现类成员函数
(1)定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
void Stock::update(double price)
(2)类方法可以访问类的private组件。
友元
C++是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。
C++设计者认为,如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。
友元提供了一种普通函数或者类成员函数访问另一个类中的私有或保护成员的机制。也就是说有两种形式的友元:
- 友元函数:普通函数对一个访问某个类中的私有或保护成员。
- 创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend:(友元函数的定义也可以放在类声明里,只有这时函数定义可以带有friend关键字)
friend Time operator*(double m,constTime&t);
- 类的友元函数是非成员函数,其访问权限与成员函数相同。
- 在友元函数定义时不用加上friend
- 友元类:类A中的成员函数访问类B中的私有或保护成员。
- 类Y的所有成员函数都为类X友元函数
class girl; class girl{ private: char *name; int age; friend boy; //声明类boy是类girl的友元 }; class boy{ public: void disp(girl &); }; void boy::disp(girl &x) //函数disp()为类boy的成员函数,也是类girl的友元函数 { //借助友元,在boy的成员函数disp中,借助girl的对象,直接访问girl的私有变量 cout<<"girl's name is:"<<x.name<<",age:"<<x.age<<endl; }
- 友元成员函数
- 类Y的一个成员函数为类X的友元函数
- 目的:使类Y的一个成员函数成为类X的友元,具体而言:在类Y的这个成员函数中,借助参数X,可以直接使用X的私有变量
- 语法:声明在公有中 (本身为函数)
- 声明:friend + 成员函数的声明
- 调用:先定义Y的对象y—使用y调用自己的成员函数—自己的成员函数中使用了友元机制
class Stock{ //class declaration private: std:: string company; long shares; double share_val; double total_val; void set_tot(){ total_val=shares* share_val;} public: void acquire(const std:: string & co, long n, double pr); void buy(long num, double price); void se11(long num, double price); void update(double price); void show(); }; class Market{ friend void Stock::acquire(const std:: string & co, long n, double pr); //Stock类下的acquire可以作为该成员函数的友元函数,可以访问该类的私有变量 int price; int fiture; public: void stuff(); };
- 利用友元函数实现重载运算符<<(也可以用于非成员重载运算符函数,本来重载了A*3,但是用户写成了3*A,此时这里可以利用友元函数实现重载(把3所属类别写在友元重载运算符函数的第一个参数即可))
#include <iostream>
using namespace std;
class Data{
private:
int A;
int B;
friend ostream& operator<<(ostream& os,const Data& data);
public:
Data(){}
Data(int a,int b):A(a),B(b){}
};
ostream& operator<<(ostream& os ,const Data& data){
os<<"A:"<<data.A<<"\tB:"<<data.B;
return os;
//返回对象使得重新定义的<<运算符能与cout和其他字符(类型)一起使用如:cout<<d<<"A"<<endl;
}
int main(){
Data d = Data(1,2);
cout<<d;//A:1 B:2
return 1;
}
友元函数和类的成员函数的区别
成员函数有this指针,而友元函数没有this指针。友元函数是不能被继承的,就像父亲的朋友未必是儿子的朋友。
23.虚函数和纯虚函数的区别(参考自https://www.runoob.com/w3cnote/cpp-virtual-function.html)
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter); 虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol “public: virtual void __thiscall ClassName::virtualFunctionName(void)”
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
定义纯虚函数就是为了让基类不可实例化化
因为实例化这样的抽象数据结构本身并没有意义。
或者给出实现也没有意义
实际上我个人认为纯虚函数的引入,是出于两个目的
1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
2、为了效率,不是程序执行的效率,而是为了编码的效率。
- 构造函数不能是虚函数的原因
- 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
- 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
- 构造函数不需要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。创建一个对象时需要确定对象的类型,而虚函数是动态联编(运行时动态确定类型)的,在构造一个对象时,由于对象未创建成功,编译器无法知道对象的实际类型。
- 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。
24.禁止拷贝构造和复制的三种方式
- 设置拷贝构造与copy assign为私有
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(){};
~Base(){};
private:
Base(const Base &);
Base &operator=(const Base &);
};
int main()
{
Base b1;
Base b2(b1);//不可访问
Base b3 = b1;//不可访问
Base& b4 = b1;//可访问
system("pause");
return 0;
}
- 继承不可拷贝构造与拷贝赋值的基类
- 因为默认生成的拷贝构造函数会自动调用基类的拷贝构造函数,如果基类的拷贝构造函数是 private,那么它无法访问,也就无法正常生成拷贝构造函数。
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(){};
~Base(){};
private:
Base(const Base &);
Base &operator=(const Base &);
};
class Drivered : private Base//继承不可拷贝与复制基类
{
};
int main()
{
Drivered d1;
Drivered d2 = d1; //不可以拷贝复制
Drivered d3(d1); //不可以拷贝构造
Drivered& d4 = d1;//可以声明引用变量
system("pause");
return 0;
}
- 使用delete
#include <iostream>
#include <string>
using namespace std;
class Uncopyable
{
public:
Uncopyable(){};
~Uncopyable(){};
Uncopyable(const Uncopyable &) = delete; // 阻止copying
Uncopyable &operator=(const Uncopyable &) = delete;
};
int main()
{
Uncopyable un1;
Uncopyable& un2 = un1;//可以声明引用变量
Uncopyable un3=un1;//Error:无法引用 函数 "Uncopyable::Uncopyable(const Uncopyable &)" (已声明 所在行数:10) -- 它是已删除的函数C/C++(1776)
Uncopyable un4(un1);//Error:无法引用 函数 "Uncopyable::Uncopyable(const Uncopyable &)" (已声明 所在行数:10) -- 它是已删除的函数C/C++(1776)
system("pause");
return 0;
}
25.using的三种用法
- 命名空间的使用;让来自命名空间的函数可以在当前作用域内可用
- using namespace std;
- using A:B;
- 在子类中引用基类的成员
class T5Base {
public:
T5Base() :value(55) {}
virtual ~T5Base() {}
void test1() { cout << "T5Base test1..." << endl; }
protected:
int value;
};
class T5Derived : private T5Base {
public:
//using T5Base::test1;
//using T5Base::value;
void test2() { cout << "value is " << value << endl; }
};
- 别名指定
typedef std::string (Foo::* fooMemFnPtr) (const std::string&);
using fooMemFnPtr = std::string (Foo::*) (const std::string&);
26.公有继承和私有继承的区别(默认的派生方式说明为private)
- 私有继承:私有继承的特点是基类中的公有成员和保护成员作为派生类中的私有成员,不能够被派生类的子类所访问。在私有继承方式下,基类中的公有成员和保护成员只能够被直接派生类访问,不能够由派生类的子类再继承。
- 公有继承:公有继承的特点是基类中的公有成员和保护成员作为派生类中的公有成员和保护成员,但基类中的私有成员仍然是私有的(不可继承)。在公有继承方式下,基类成员的访问权限在派生类中保持不变,公有派生类的对象可以访问基类中的公有成员和保护成员;派生类的成员函数可以访问基类中的公有成员和保护成员。
- 保护继承:特点是基类中的公有成员和保护成员都作为派生类中的保护成员,基类中私有成员仍然是私有的。
27. Map的遍历方式
- 使用迭代器遍历
#include <map>
#include <iostream>
std::map<int, string> m;
for (std::map<int, string>::iterator it = m.begin(); it != m.end(); ++it) {
//key
std::cout << it->first << std::endl;
//value
std::cout << it->second << std::endl;
}
- 键值对遍历(C++17)
#include <map>
#include <iostream>
//...
std::map<int, string> m;
//...
for (const auto &[key, value] : m) {
//key
std::cout << key << std::endl;
//value
std::cout << value << std::endl;
}
28.关于内联函数https://www.bilibili.com/read/cv8682976/
- 1、函数定义前带inline;2、类内定义和声明在一起的函数。
- 内联函数与常规函数之间的主要区别不在于编写的方式,而在于C++编译器如何将它们组合到程序中。对于内联函数,编译器将使用相应的代码替换函数调用,也就是将函数体中的代码嵌入到被调用的地方去。这样处理,使得程序无需跳转到另一个位置执行代码,再跳回来,从而加快运行速度。
- 编译结果里面并不会出现那个函数,内联函数仍旧保持了函数的独立性(函数有自己的空间,有自己的变量,函数调用时需要类型检查),只是不去真正地调用函数而已,函数类型检查这个步骤依然存在(由编译器完成),因而不会增加程序运行时的负担。
- 注意事项:对于一般的函数,其声明放在.h文件中,实现放在.cpp里,在其他文件里使用该函数时,只需要包含.h文件即可。inline函数可以这样做吗?不可以,编译会报错。如下:
//a.cpp
#include<iostream>
inline void fun(int a, int b){
using namespace std;
cout <<"a="<<a<<"b="<<b<<endl;
}
//a.h
inline void fun(int a, int b);
//main.cpp
#include "a.h"
int main(){
fun(1,2);
return 0;
}
- 为什么会报错,因为在编译主函数时,根据头文件知道有一个函数是inline,但是不知道这个函数的body,因此无法按照inline的规定将其函数体的代码插入到主函数中,就将这个函数当成了普通的函数。而在编译该内联函数的.cpp时,由于编译器发现是一个内联函数,就不会生成相应的代码。因此链接器在链接两个源文件时报错,main需要调用函数,而那个函数却不存在。
- 因此内联函数的body应该直接写在头文件里(不需要对应的.cpp),然后被包含到其他的源文件,此时的内联函数不再是定义,其实就是个声明。
- 要不要用内联函数
- 内联函数的代码会被插入到调用处,如果程序有好几处调用该函数,程序就会变长,因此内联函数牺牲了代码的空间,降低额外运行开销,加快了程序运行的速度。
- 内联函数比C语言的宏定义好,如 :#define SQUARE(X) XX a=SQUARE(5.0) 即为a=5.05.0,这并不是通过值传递实现的,而是通过文本替换实现的,假如输入的参数不是5.0,而是一个字符串呢?这种宏定义不能进行类型检查,而内联函数可以,因此更安全。
- 如果编译器发现函数很长,会自动拒绝该函数作为内联函数。
- 递归不能作为内联函数,因为递归一定需要借助堆栈的压入和弹出实现功能。
- 对于C++类的成员函数,如果在函数声明的地方就给出了函数体,这些函数默认就是内联函数,如:
class fruit{ int color; public: int getColor(){return color;} void setColor(int color){ this->color = color; } }
- 类的成员函数“getColor()”和“setColor()”是内联函数,它们的调用相当于直接访问属性“color”,运行效率较高,但是通过函数去访问类的属性,符合面向对象编程的思想,函数使得类的属性与外界产生隔绝。
- 如果函数很小,又在程序中多次被调用(如一个循环中),编译器会自动将其作为inline函数。
29.explicit关闭将构造函数用作自动类型转换函数(隐式转换)的特性
- 在构造函数声明前加上explicit可以关闭这种将将构造函数用作自动类型转换函数(隐式转换)的可能,只允许显示转换(即强制类型转换)
- 只接收一个参数(或多个参数,但除第一个参数以外其他参数都具有默认值)的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显式转换,否则可以用于隐式转换。
class stonewt{
public:
...
explicit stonewt(double lbs);
private:
...
}
- 如果没有explicit,stonewt可以用于隐式转换的情况有
- 将stonewt对象初始化为double值;
- 将double值赋给stonewt对象;
- 将double值传递给接受stonewt参数的函数;
- 返回值被声明为stonewt对象的函数返回double值;
- 在上述任意一种情况下,使用可转换为double类型的内置类型时(如stonewt one = 7300;如果没有stonewt(long/int)的构造函数,那么就会将7300转换为double,然后使用stonewt(double)构造函数,当然这种情况只发生在转换不存在二义性的情况下)
- 但是若同时存在stonewt(double)和stonewt(long)构造函数,stonewt one = 7300可能出现二义性,因此为了防止二义性应使用explicit
30.左值、右值和引用(https://www.cnblogs.com/Bylight/p/10530274.html)
- 左值(lvalue):一个标识非临时性对象的表达式。通常来说,可以将程序中所有带名字的变量看做左值。
- 右值(rvalue):相对的,右值标识是临时性对象的表达式,这类对象没有指定的变量名,都是临时计算生成的。
- 我们可以这样理解引用:一个引用是它所引用对象的同义词,是其另一个变量名。
- 左值引用:
- 左值引用的声明是通过在某个类型后放置一个符号&来进行的。前文代码中的int & y = x;便是一个左值引用。
- 需要注意的是,在定义左值引用时,=右边的要求是一个可修改的左值。因此下面几种左值引用都是错误的:
#include <stdio.h> int main() { const int x = 5; int y = 1; int z = 1; int & tmp1 = x; // ERROR:x不是一个可修改的左值 int & tmp2 = 5; // ERROR:5是一个右值 int & tmp3 = y + z; // ERROR:y+z是一个右值 return 0; }
- 右值引用:
- 类似于左值引用,右值引用便是对右值的引用,它是通过两个&&来声明的。
#include <stdio.h> int main() { int && x = 5; printf("x = %d\n", x); return 0; }
- 引用和指针的区别
- 我们知道,指针是在内存中存放地址的一种变量,cpu能够直接通过而变量名访问唯一对应的内存单元,且每个内存单元的地址都是唯一的。
- 而变量名和引用,都可以看做内存的一个标签或是标识符,计算机通过是否符合标识符判断是否为目标内存,而一个内存可以有多个标识符。
- 左值引用的用途
- 作为复杂名称变量的别名 auto & whichList = theList[myHash(x, theList.size())];
- 用于rangeFor循环
- 设想我们希望通过rangeFor循环使一个vector对象所有值都增加1,下面的rangeFor循环是做不到的
for (auto x : arr) // x仅相当于每个元素的拷贝 ++x;
- 但我们可以通过使用引用达到这一目的
for (auto & x : arr) ++x;
- 避免复制大的对象
- 假定有一个findMax函数,它返回一个vector中最大的元素。若给定vector存储的是某些大的对象时,下述代码中的x拷贝返回的最大值到x的内存中: auto x = finaMax(vector); 在大型的项目中这显然会增大程序的开销,这时我们可以通过引用来减小这类开销 auto & x = findMax(vector); 类似的,我们在处理函数返回值的时候也可以使用传引用返回。但是要注意,当返回的是类中私有属性时,传回的引用会导致外界能够对其修改。
- 参与函数中的参数传递
- 在C和C++的函数中,addSelf(int x)这类函数对直接传入的参数进行修改并不会改变原有参数的值。而有时我们希望能够实现类似swap(int a, int b)这类能够修改原参数的函数时,我们可以通过1.传入指针和2.传入引用实现。
31.关于基类指针/引用指向/引用派生类对象的问题
- 基类指针/引用可以指向/引用派生类对象(向上强制转换upcasting,可隐式转换),但是如果基类不存在和派生类特定成员函数同名同参数列表(特征标)的“虚函数”,该指针或引用就不能调用派生类的这个函数:
class A{
public:
int func1(){
cout<<"class A func1";
return int1;
}
private:
int int1;
};
class B: public A{
public:
int func2(){
cout<<"class B func1";
return int2;
}
private:
int int2;
};
int main(){
A* a = new A();
a->func1();
A* b = new B();
b->func2();//Error:类 "A" 没有成员 "func2"
}
- 但如果基类存在和派生类特定成员函数同名同参数列表(特征标)的“虚函数”,该指针或引用就能调用派生类的这个函数。(使用virtual关键字表示该同名同参函数后,程序将根据引用或指针指向的对象的类型来选择方法)
class A{
public:
// virtual int func1()=0;//若A的func1为纯虚函数,那么该类为抽象类,不能创建对象
virtual int func1(){//
cout<<"class A func1";
return int1;
}
private:
int int1;
};
class B: public A{
public:
virtual int func1(){
cout<<"class B func1";
return int2;
}
private:
int int2;
};
int main(){
A* a = new A();
a->func1();//"class A func1"
A* b = new B();
b->func1();//"class B func1"
}
-
将基类指针或引用转换为派生类指针或引用(或者说是用派生类指针或引用指向或引用基类对象,称为向下强制转换downcasting)必须使用显式类型转换:Singer* ps = (Singer *)Employee;,同时注意不能调用派生类新增的成员函数。
-
关于动态联编:编译器对于虚函数将会在每个类对象增加一个虚函数数组(指向函数地址)空间,派生类如果没重新实现则会沿用基类的虚函数放入该空间内,但重新实现的则会覆盖之前基类实现的虚函数。并且,若派生类新实现的函数参数和基类该虚函数的参数不同,不具有重载特性(即不能对派生类对象调用基类函数,因为派生类新视线的函数会隐藏所有的同名基类方法,即虚函数数组空间不会有基类函数的虚函数地址);而且可以实现返回类型协变(covariance of return type, C++11),即允许重新定义基类函数时可以修改返回类型从基类引用/指针为派生类引用/指针。
-
可以将派生类对象赋给基类对象,但只有基类成员被赋值;但如果及不存在将基类对象转换成派生类对象的构造函数,也不存在将基类对象赋给派生对象的赋值运算符重载,那么不可以将基类对象赋给派生对象。
-
对于虚函数导致的动态联编(晚期联编),只对使用指针和引用作为参数传入函数生效,对于按值传递作为函数参数(会生成对应类对象副本的)不适用,就调用不了派生类(或基类)的函数了。
-
成员函数属性小结(op=表示诸如+=、*=等格式的赋值运算符,这类运算符特征和其他运算符类别没有区别,单独列出op=旨在指出这些运算符与=运算符的行为是不同的;友元函数虽然不能继承,但派生类对象可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后调用基类的友元函数,或者dynamic_cast<(const)baseclass&/*>(obj))
函数 | 能否继承 | 成员还是友元 | 默认能否生成 | 能否为虚函数 | 是否可以有返回类型 |
---|---|---|---|---|---|
构造函数 | 否 | 成员 | 能 | 否 | 否 |
析构函数 | 否 | 成员 | 能 | 能 | 否 |
= | 否 | 成员 | 能 | 能 | 能 |
& | 能 | 任意 | 能 | 能 | 能 |
转换函数 | 能 | 成员 | 否 | 能 | 否 |
() | 能 | 成员 | 否 | 能 | 能 |
[] | 能 | 成员 | 否 | 能 | 能 |
-> | 能 | 成员 | 否 | 能 | 能 |
op= | 能 | 任意 | 否 | 能 | 能 |
new | 能 | 静态成员 | 否 | 否 | void* |
delete | 能 | 静态成员 | 否 | 否 | void |
其他运算符 | 能 | 任意 | 否 | 能 | 能 |
其他成员 | 能 | 成员 | 否 | 能 | 能 |
友元 | 否 | 友元 | 否 | 否 | 能 |
32、C++11标准库提供的begin,end函数
- 给普通数组使用时可以提供数组元素地址(指针)而不是容器的迭代器
int res[]={0,1,2,3,4,5};
int *p = begin(res), *pe = end(res);//begin/end Return an iterator pointing to the first/last element of the array.
cout<<p<<":"<<*p<<endl;
cout<<pe<<":"<<*pe<<endl;
- 给容器使用时提供的也是迭代器
vector<int> res2 = {9,8,5,3,1};
vector<int>::iterator ps = begin(res2), pt = end(res2)-1;//注意end是空值(容器均为左闭右开)
//相当于 vector<int>::iterator ps = res2.begin(), pt = res2.end()-1;
cout<<"iterator ps:"<<*ps<<endl;
cout<<"iterator pt:"<<*pt<<endl;