局部变量:在函数内定义的变量,其作用域仅限于函数内。
全局变量:作用域从定义处开始,到所在文件的结束。
外部变量:某个文件中(main.cpp)引用另一个文件中的全局变量(用extern声明)格式如下:
extern类型说明符 外部变量名;
例如:file1.cpp 下定义了全局变量 int i; main.cpp中需要用到i则做 extern int i;
1.变量声明和定义区别?
声明仅仅规定的变量的类型和名字,并不分内存空间;定义要为其分配存储空间。
相同变量可以在多处声明(外部变量extern),但只能在一处定义。
2.“零值比较”
bool类型:if(flag) //flag为真 if(!flag) //为假bool类型定义零值记FALSE,非零值记TRUE(TRUE究竟是什么并没有统一的标准)bool类型定义零值记FALSE,非零值记TRUE(TRUE究竟是什么并没有统一的标准)
int类型:if(flag == 0) if(flag != 0)
指针类型:if(flag == NULL) if(flag != NULL) //尽管NULL的值与0相同,两者意义不同空地址和空值
float类型:if((flag >= -0.000001) && (flag <= 0.000001)) //0.000001为允许误差
3.strlen和sizeof区别
sizeof是运算符,结果在编译而非运行中获得,strlen是函数:求字符串实际长度(不包括'\0')
sizeof参数可以是任何数据类型也可以表达式,strlen只能是字符指针
char s[3]="kjs";
int len = strlen(s);
4.深拷贝浅拷贝
拷贝构造函数:
class Object
{
public:
Object(const Object& other) //拷贝构造函数参数特殊
{
}
};
拷贝构造函数调用:和普通构造函数一样,总被编译器隐含地调用(三种方式)
定义对象:
Object a;
Object b(a);
动态创建对象:
Object a;
Object* p = new Object(a);
函数的传值调用:
void Test(Object obj);
默认的拷贝构造函数:通常编译器会默认配置一个拷贝构造函数
#include <stdio.h>
#include <string>
#include <iostream>
using namespace std;
class Object
{
public:
Object(int id, const string name_in)
{
this->id = id;
name = name_in;
}
private:
int id;
string name;
};
int main()
{
Object a(1, "some");
Object b(a);
return 0;
}
定义拷贝构造函数注意事项:
1首先调用父类的拷贝构造函数 2依次构造每一个成员
深拷贝浅拷贝
默认的拷贝函数使用的进行浅拷贝,即只对指针复制,没有复制内存。
深拷贝申请了新的内存。
5.结构体内存对齐问题
结构体内成员按照声明顺序存储,第一个成员和整个结构体地址相同
未特殊说明,按结构体中size最大的成员对齐(若有double成员),按8字节对齐(N字节对齐,内存地址能被n整除)。
6.static作用是什么?在C和C++中有何区别?
static可以全局变量(静态全局变量)和函数(静态函数),未初始化时,static默认值为0。
修饰全局变量:变量声明周期不变,但该变量名字被限制在cpp页面内可见(无法用extern来声明并访问它)。
修饰局部变量:性质仍然是全局变量,但仅在函数内访问
#include <stdio.h>
#include <iostream>
using namespace std;
void Test()
{
static int a = 0;
a++;
cout << a << endl;
}
int main()
{
Test(); //输出为1
Test(); //输出为2
return 0;
}
修饰函数:只在本文件中可见,限制访问域。
7.结构体和类的区别?
struct的默认限定是public,外部可直接读取和修改它。类默认是private。
8.malloc和new的区别?
int* p = (int*)malloc(400);
free(p);
TYPE* p = new TYPE[N]; //创建N个对象
delete [] p;
malloc和free为标准库函数,new和delete是运算符。
malloc仅仅分配内存,free仅仅回收空间,不具备构造和析构函数功能,用malloc分配空间成员未初始化;
new和delete除了分配回收功能外,还会调用构造函数和析构函数。(delete:先调用相应析构函数,再释放相应内存)
malloc和free返回的是void类型指针,new和delete返回的是具体类型指针。
9.指针和引用区别?
引用只是别名,不占具体存储空间,只有声明没有定义。指针是具体变量,需要占用存储空间。
引用声明时必须初始化,之后不可再改变。指针变量可以重新指向别的变量。
不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
指针相关概念:
- 星号操作:加 * 直接读取内存值。
- 指针与数组的转换:
int arr[4] = {1, 2, 3, 4};
int *p = &arr[1]; //p指向arr[1]
p[0] = 0xAA; //p[0]:自p开始的第0个元素,即arr[1]
int* a[4] : 指针数组
表示:数组a中的元素都为int型指针 元素表示:*a[i] *(a[i])是一样的,因为[]优先级高于*
int (*a)[4] : 数组指针
表示:指向数组a的指针 (例如:arr[][]为二维数组,**a指向第一行第一个元素,**(a+1)指向第二行第一个元素) 元素表 示:(*a)[i]
10.宏定义和函数有何区别?
#define Max(a,b) a>b?a:b //建议用inline函数代替这种宏替换
宏在编译预处理时完成替换,函数再运行时候调用。
宏无返回值,函数调用可有具体返回值。
宏函数最后不要加分号,数据不分配内存空间。
11.宏定义和const区别?
12.宏定义和typedef区别?
typedef:是C++关键字,用来声明已有数据类型的别名(typedef int INT;)
typedef在编译过程中,#define在编译阶段之前(无;)
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1,p2; //一个指针,一个整型变量
INTPTR2 p3,p4;
13.宏定义和内联函数区别?
inline int max(int a, int b) //发现函数体较短时,声明inline,编译器会做特定优化
{
return a>b?a:b;
}
14.条件编译#ifdef,#else,#endif作用?
#ifndef:如果对应宏没有定义,则相应的代码被编译。头文件可以被重复包含
15.区别以下几种变量?
const int a;
int const a; //定义常量类型a
const int *a; //a指向不可变常量
int *const a; //a为指向整型数据的常指针
16.volatile有什么作用?
volatile定义变量的值是易变的,对象的值可能在程序的控制或检测之外被改变(应该申明为volatile)。
线程中被几个任务共享的变量要定义为volatile类型。
17.什么是常量引用?
原变量值不会被别名所修改,可以通过原名修改。
通常用作只读变量别名或者形参传递。
18.区别以下指针类型?
指针概念:
int a = 1;
int* p = &a; //int* 作为一个整体使用,称指针类型
星号操作:
用于作用指针变量,来直接读写内存的值。
指针与数组arr[4]={0};
数组名arr代表的就是这一块内存的首地址。
指针加减法:指针变量加减时,是以元素为单位进行移动。
int* p = arr;
p += 2; //指向arr[2]的地址 同:arr + 2;
指针与数组的转换(可把p当成数组使用)。
int* p = &arr[1];
p[0] = 0xAA; //p[0]:自p开始的第0个元素,即arr[1]
p[1] = 0xBB; //arr[2]
19.常量指针和指针常量区别?
常量指针:int const *p或const int *p,常量的指针,指向一个只读变量。
指针常量int *const p:不能改变指针的指向。
20.a和&a有什么区别?
int a[10];
int (*p)[10] = &a;
21.数组名和指向数组首元素的指针区别?
二者均可以通过加减来访问数组中的元素。
数组名不是真正意义上的指针,无自增、自减等操作。
数组名当成形参传递给调用函数后,退化成一般指针,多了自增、自减操作,但sizeof运算符不能得到原数组的大小。//sizeof(a);
22.野指针是什么?
不指向null,指向垃圾内存的指针。
指针变量未及时初始化(解决:定义指针变量初始化,要么置空)。
指针free或delete之后没有及时置空(解决:释放操作后立即置空)
23.堆和栈的区别?
24.delete和delete [] 区别?
delete只调用一次析构函数。
delete [] 会调用数组中每个元素的析构函数。
面向对象基础
C程序员向C++程序员进阶的基础。
1.面向对象三大特性?
封装性:内部东西封闭起来,若想访问,需要通过指定的函数接口。
继承性:某种类型对象获得另一个类型对象的属性和方法。
多态性:向不同对象发送同一消息,不同的对象在接受时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。
关于class的一些说明
类和成员函数(定义在class内的函数)
this指针:默认每个成员函数传入一个this指针,指向这个对象本身。
class Object
{
public:
void Set(int a, int b)
{
this->x = a;
//this->y = b;
y = b; //this->调用可以忽略不写
}
private:
int x;
int y;
};
构造函数:形式:(1)函数名与类名相同。(2)没有返回值。作用:对新建的对象进行初始化,避免错误。
重载构造函数:构造函数可以被重载,只要参数列表不同就可以。
与普通函数不同,在创建一个对象时,构造函数被自动调用(由编译器完成)。
析构函数作用:对象的销毁过程,当对象生命周期结束时,它的析构函数被自动调用。
释放资源
class Test
{
public:
~Test()
{
if(m_buf) free(m_buf); //释放资源
}
//其他部分代码省略
};
自动生成构造/析构函数:如果没写构造或析构,编译器会隐含地添加一个。
默认构造函数:不需要传参的函数
构造函数成员变量初始化1.直接复制2.初始化列表
class Circle
{
public:
Circle():a(0), b(0)
{}
private:
int a;
int b;
};
必须使用初始化列表的情况:
1.无默认构造函数
2.成员为引用类型时
class Object
{
public:
Object(int &a):ref(a)
{}
private:
int& ref; //引用的用法:int a = 0; int &r = a;
};
int main()
{
int value = 1;
Object obj(value);
return 0;
}
2.访问修饰符public/protected/private的区别?
public:成员可在外部访问,可继承。
protected:成员不能被外部访问,可继承。
private:成员不能被外部访问,不可继承。
3.对象存储空间?
4.C++空类有哪些成员函数?
C++中空类大小为1字节。空结构体也占一个字节。
默认函数有:
构造函数
析构函数
拷贝构造函数
赋值函数: Class & operator=(const Class & other)
继承的相关概念
成员函数的重写:从父类继承过来的函数,子类重新写一遍,可用新的访问修饰符修饰。
class Base
{
protected:
void Test()
{
}
};
class Child : public Base
{
public:
void Test() //重写
{
}
};
虚函数virtual:
将父类的Test加virtual关键字修饰,这个函数就成为“虚函数”。
class Base
{
public:
virtual void Test() //加上virtual关键字
{
printf("父类Test...\n");
}
};
class Child : public Base
{
public:
void Test()
{
printf("子类Test...\n");
}
};
int main()
{
Base* p = new Child;
p->Test(); //加virtual 输出:子类Test...
return 0;
}
注意:
如果父类函数声明virtual,那么子类重写时可以将virtual省略。子类这个函数自动为virtual。
如果一个成员函数在设计时就确定要被子类重写,那么就应该将其申明为virtual。
virtual析构函数:
在继承关系中,父类的析构函数应该被声明为virtual,否则会导致子类没有正确析构。
Base* p = new Child;
delete p;
继承关系下的构造与析构过程:
先调用父类的构造函数,在执行自己的构造函数。子类对象析构时,过程相反。
纯虚函数
语法:(1)将成员函数声明为virtual。(2)后面加 =0 。(3)该函数不能有函数体 如:
class Cmdhandler
{
public:
virtual void Cmdhandler(char* cmd) = 0;
};
包含纯虚函数的类称为纯虚类。常纯虚函数的称为抽象函数,纯虚类称为抽象类。
纯虚不能被实例化。 如: Cmdhandler handler; //会直接报错 那么它的作用?
用于实现“接口”的概念。
5.构造函数能否为虚函数,析构函数呢?
析构函数可以为虚函数,并且一般情况下父类析构函数要定义为虚函数,在delete时,才能准确调用子类析构函数。
析构函数可以是纯虚函数,含有纯虚函数的类是抽象类,此时不能被实例化。但派生类中可以根据自身需求重新改写基类中的纯虚函数。
构造函数不能定义虚函数。在构造函数中可以调用虚函数,不过此时调用的是正在构造的类中的虚函数,而不是子类的虚函数,因为此时子类尚未构造好。
6.构造函数与析构函数调用顺序?
构造:构造成员->成员的构造函数->自己构造函数
析构:自己析构函数->成员的析构函数
7.拷贝构造函数中的深拷贝和浅拷贝?
深拷贝:动态分配的存储空间,先动态申请一块存储空间,然后逐字节拷贝内容。
浅拷贝:拷贝指针字面值。
当使用浅拷贝时,如果原来的对象调用析构函数释放掉指针所指向的数据,则会产生空悬指针。因为所指向的内存空间已经被释放了。
8.拷贝构造函数和赋值运算符重载的区别?
拷贝构造函数是函数,赋值运算符是运算符重载。
拷贝构造函数会生成新的类对象,赋值运算符不能。
9.虚函数和纯虚函数区别?
格式不同,纯虚函数不能被实例化,相当于一个接口名。
10.覆盖、重载和隐藏的区别?
重载:统一访问区内被声明几个同名函数(但参数列表不同如,重载构造函数)
重写(覆盖)对父类的补充:子类中存在重新定义的函数。函数名,参数列表,返回值类型需与父类被重写函数一致。访问修饰符,函数体可不同(花括号内)。
隐藏:指子类函数屏蔽了与其同名的父类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。(隐藏的范围比覆盖范围更宽)
- 覆盖的是指子类函数覆盖父类函数
在不同的类内,同名同参。
基类的函数名前必须有virtual关键字。
- 隐藏指派生类的函数隐藏了基类的同名函数
如果派生类函数与基类函数同名,但参数不同,无论基类函数前是否有virtual修饰,基类函数被隐藏。
如果派生类函数与基类函数同名,参数也相同,但是基类函数前无virtual修饰,基类函数被隐藏。
11.在main函数执行之前的代码可能是什么?
全局对象的构造构造函数(核心会运行专门的启动代码,启动代码会在启动main()之前完成所有初始化工作,包括全局对象的初始化)
12.哪几种情况必须用到初始化列表?
类成员为const类型
类成员为reference类型
调用一个基类的构造函数,而该函数有一组参数。
调用一个数据成员对象的构造函数,而该函数有一组参数。
class Base
{
public:
Base(const string &str = "", int i = 0) : Bstr(str), _i(i) // 使用const引用避免复制,
// 如果只使用const则无法使用字面常量"DerivedStr"为str赋值
{
cout << "Base Constructor" << " Bstr = " << Bstr << ", _i = " << _i << endl;
}
string Bstr;
int _i;
};
class Derived : public Base
{
public:
// 调用基类构造函数,而它拥有一组参数时,要使用成员初始化列表
Derived() : Base("DerivedStr", 200) // 这个是正确的
{
//Base::Bstr = "DerivedStr"; // 基类构造函数再次之前调用,这里赋值没有用。
//Base::_i = 200;
cout << "Derived Constructor" << endl;
}
string Dstr;
};
13.什么是虚指针?
虚指针或虚函数指针是一个虚函数的实现细节。指向虚函数表。C++中,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表。
有时候,我们得提供一些接口给别人使用。接口的作用,就是提供一个与其他系统交互的方法。其他系统无需了解你内部细节,并且也无法了解内部细节,只能通过你提供给外部的接口来与你进行通信。根据c++的特点,我们可以采用纯虚函数的方式来实现。这样做的好处是能够实现封装和多态。
Class IPerson
{
public:
IPerson() {};
virtual ~IPerson()=0 {}; //注意,最好定义此虚析构函数,够避免子类不能正常调用析构函数;如果定义为纯虚析构函数,则必须带定义体,因为子类隐含调用该析构函数。
//提供给外面使用的接口一般采用纯虚函数
virtual void SetName(const string &strName)= 0;
virtual const string GetName()= 0;
virtual void Work()= 0;
}
14.重载和函数模板的区别?
重载的函数函数名相同,但参数列表不同。
函数模板用于不同类型变量实现相同的算法或逻辑。
15.this指针是什么?
默认每个成员函数传入一个this指针,指向这个对象本身。
16.类模板是什么?
模板的提出是为了表达通用的算法或逻辑。类模板:作用于类
template<typename T> //用于声明此函数是一个模板,T是一个类型名
T findmax(T arr[], int len)
{
T val = arr[0];
for (int i = 1; i < len; i++)
{
if (arr[i] > val) val = arr[i];
}
return val;
}
int main()
{
int arr[4] = {1, 2, 3, 4};
int result = findmax<int>(arr, 4); //实例化
return 0;
}
标准模板库(STL广义上讲分三类:容器、数据、算法,三者有效分离)
STL内容很多,单独成书《STL源码剖析》,实际使用,需要知道以下几点:
- 怎么用?
各种STL基本的增删改查怎么使用。掌握基本功能,放在对需求的了解并选择合适的数据结构。
- 怎么实现?
本身STL封装了常用的数据结构,所以最先需要了解每种数据结构的特性。
- 如何避免错误?
已经知道他们底层使用的是什么数据结构以及该数据结构做什么操作比较高效。之后需要注意怎样避免未知错误,如迭代器失效问题。
string
初始化
string s1; //默认初始化,s1是一个空字符串
string s2(s1); //s2是s1的副本
string s2 = s1; //与上等价,赋值,使用(=)实际执行拷贝初始化
string s3("value"); //直接初始化
string s3 = "value"; //s3是该字符串面值的副本
string s4(10, 'c'); //s4的内容
运算:
输入
>> -> getline(cin, line) //getline遇见空格保存空格不停止
比较:
运算符(==和!=):长度相等而且做包含的字符也全部相同。
运算符(<、<=、>、>=):比较string对象中第一对相异字符
相加(加法运算符+、复合赋值运算符+=):
string s1 = "kjs", s2 = "king"; //字符字面值与字符串字面值混在一起时,保证+两侧对象至少有一个string
string s3 = s1 + "is" + s2 + '/n';
string s6 = (s1 + ", ") + "king"; //正确
字符处理:for + 迭代器 + 一组标准库函数如:toupper(c)
//处理全部字符 for(declaration:expression)
string str("some string");
for(auto &c : str)
c = toupper(c); //如果c是小写字母,输出大写
cout << str << endl;
//处理部分字符 下标、迭代器
string s("some string");
if(!s.empty()) //不管什么时候对string对象使用下标,都要确定该位置是否有值
s[0] = toupper(s[0]);
cout << s << endl;
//迭代器 &&:逻辑运算符 左侧为真才会检查右值(下标在合理范围式才会用此下标访问字符串)
string s("some string");
for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]);
cout << s << endl;
其它:
line.empty(); //empty根据string对象是否为空返回对应布尔值
line.size(); //string对象的长度 类型:string::size_type(标准库类型定义了几种) -> auto(用来对变量类型的推断)
vector
用法:
定义:
vector<T> vec; //vector<int> vec
插入元素:
vec.push_back(element);//当size==capacity时,重新申请一个较大的缓冲区,原数据复制到新缓冲区,在追加新元素。
vec.insert(iterator, element); //iterator插入位置
删除元素:
vec.pop_back();
vec.erase(iterator);
修改元素:
vec[position] = element;
遍历容器:
//vector<int>::interator it; 与vector<int>::const_interator it; //it只能读不能写
for(auto it = vec.begin(); it != vec.end(); ++it)
{
int& p = *it; //*it :解引用
}
其它:
vec.empty(); //判断是否为空
vec.size(); //实际元素
vec.capacity(); //容器容量
vec.clear(); //用于清空元素,size归0,而capacity不变
vec.begin(); //获得首迭代器
vec.end(); //获得最后一个元素下一个位置的迭代器
vec.clear(); //清空
实现:
线性表,数组实现。
支持随机访问
插入删除操作需要大量移动数据
需要连续的物理存储空间
每当大小不够是,会重新分配内存 vec.resize();
错误避免(某些操作使迭代器失效)for循环中改变vector容量,如push_back
插入元素:
尾后插入:size<capacity时,首迭代器不失效尾迭代器失效,size==capacity时,所有迭代器都失效(重新分配了空间)
map用法
定义:
map<T_key, T_value> mymap;
插入元素:
mymap.insert(pair<T_key, T_value>(key, value)); //同key不插入
mymap.insert(map<T_key, T_value>::value_type(key, value));
mymap[key] = value;
删除元素:
mymap.erase(key); //按值删除
mymap.erase(iterator); //按迭代器删
修改元素:
mymap[key] = new_value;
遍历容器:
for(auto it = mymap.begin(); it != mymap.end(); ++it)
{
cout << it->first << " => " << it->second << '\n';
}
实现:
- 树状结构,RBTree实现
插入删除不需要数据复制
操作复杂度仅跟树高有关
- RBTree本身也是二叉排序树的一种,key值有序,且唯一。
必须保证key可排序
基于红黑树实现的map结构(实际上是map, set, multimap,multiset底层均是红黑树),不仅增删数据时不需要移动数据,其所有操作都可以在O(logn)时间范围内完成。另外,基于红黑树的map在通过迭代器遍历时,得到的是key按序排列后的结果,这点特性在很多操作中非常方便。
enum Color {
RED = 0,
BLACK = 1
};
struct RBTreeNode {
struct RBTreeNode*left, *right, *parent;
int key;
int data;
Color color;
};
平衡二叉树(AVL数):是一种二叉排序树,但多了一个条件(每一个节点左子树和右子树高度差至多等于1)。
所以对红黑树的操作需要满足两点:1.满足二叉排序树的要求;2.满足红黑树自身要求。通常在找到节点通过和根节点比较找到插入位置之后,还需要结合红黑树自身限制条件对子树进行左旋和右旋。
unordered_map
C++中的哈希表,可以在任意类型与类型之间做映射。
基本操作
引用头文件(C++11):#include <unordered_map>
定义:unordered_map<int,int>、unordered_map<string, double> ...
插入:例如将("ABC" -> 5.45) 插入unordered_map<string, double> hash中,hash["ABC"]=5.45
查询:hash["ABC"]会返回5.45
判断key是否存在:hash.count("ABC") != 0 或 hash.find("ABC") != hash.end()
遍历
for (auto &item : hash)
{
cout << item.first << ' ' << item.second << endl;
}
//或
for (unordered_map<string, double>::iterator it = hash.begin(); it != hash.end(); it ++ )
{
cout << it->first << ' ' << it->second << endl;
}
示例:
#include <stdio.h>
#include <string>
#include <map>
#include <iostream>
using namespace std;
void Count(map<string, int> &word_count)
{
for (const auto & w : word_count)
cout << w.first << w.second << endl;
}
int main()
{
map<string, int> word_count;
string word;
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> word;
++word_count[word];
}
Count(word_count);
return 0;
}