#include
把iostream标准库文件的内容添加到其所在文件中的#include
位置int greater(intn a, int b)
定义了main函数的返回类型、函数名、输入参数。变量初始化的3种形式:初始化列表、函数表示法、赋值表示法,建议用哪一种,原因
答:
初始化列表初始化:int apple_count {15};
函数表示法:int orange_count (5);
赋值表示法:int total_fruit = apple_count + orange_count;
建议使用初始化列表进行初始化,可更好避免缩窄转换(float ⇒ \Rightarrow ⇒ int)。
为什么最好在单个语句中定义每个变量?
答:指不用int a{1}, b{2}, {3};
形式。因为在单个语句中定义每个变量可提高代码的可读性。
什么是字面量?说明各种字面量的类型
答:指各种类型的常量。每个字面量都有特定的类型。
常量 | 字面量 | 类型(const) | 语句 |
---|---|---|---|
-123 | 整型字面量 | int | int value{-123}; |
123’456’789LL | 整型字面量 | long | long long distance {123'456'789LL}; |
123u | 整型字面量 | unsighed short | unsighed short price {123u}; |
2.3245 | 浮点型字面量 | float | float factor {2.3245}; |
‘Z’ | 字符字面量 | char | char alph {'Z'}; |
“number” | 字符串字面量 | char [] | char name[] {"Mae West"}; |
ture | 布尔字面量 | bool |
为什么建议仅在有充足理由时使用using指令?
答:使用using指令声明名称空间后,using std::cout;
,就不需要使用名称空间限定名称了,这种用法过多会增加名称冲突的可能性。
sizeof运算符及其结果类型
答:可对类型、变量、表达式使用,得到其所占用的字节数,结果类型为size_t;
size_t不是内置的基本类型名称,而是标准库定义的一个类型别名;
size_t是一个不带符号的整数,可存储任何类型理论上可能存在的对象的最大大小,常用于数组索引和循环计数。
递增递减运算符的前后缀形式的区别
答:前缀先计算值再计算表达式,后缀先计算表达式后计算值
浮点类型什么时候用double,什么时候用float?
答:大多数情况下,使用double类型就足够了,只有速度或数据大小非常关键时,才会使用float。
数学函数头文件
答:cmath头文件定义了许多三角函数和数值函数,所有函数名都在std名称空间中定义。函数的结果总是与浮点型参数的类型相同,整型参数的结果为double类型。
输出流格式化头文件(2个)及其区别
答:iostream头文件,无参数
iomanip头文件,有参数
什么是显式类型转换,为什么需要它?如何实现?
答:把表达式的值显式转换为给定类型static_cast
,显式强制转换以得到希望的结果类型。static_cast表示进行该强制转换要进行静态检查。
如何确定数值的上下限?
答:std::numeric_limits
、std::numeric_limits
字符变量(char类型)是否可参与算术表达式?
答:可以,因为char类型的变量是数值,它们存储了表示字符的整数代码,所以它们可以参与算术表达式。
auto关键字的使用场景,是否推荐用于定义基本类型变量?为什么?
答:用于推断类型,建议只用于推断很长的自定义类型、长的指针等,定义基本类型的变量应显式指定类型。
运算符的执行顺序和什么有关?
答:执行顺序由运算符的优先权决定。
枚举类型的定义及使用枚举类型定义变量的语句实现
答:enum class Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
Day today {Day::Tuesday};
要输出today的值,就必须先把它转换为数值类型,因为标准输出流不能识别自定义类型std::cout << "Today is " << static_cast
新旧枚举类型定义方法的不同及新方法的优势
答:旧语法 enum Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
C++11后新语法enum class Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
新语法在定义时加入class关键字,使得枚举成员在转换为整型甚至浮点类型时,会进行强制转换,更不容易出错。
枚举类型成员的默认值及成员显式赋值
答:1)第一个枚举成员的值默认为0,其后的成员依次加1;
2)枚举成员不一定有唯一值,且不一定必须以升序进行赋值;
可以根据以前的枚举成员定义新的枚举成员值。enum class Day {Sunday=7, Monday=1, Tuesday=Monday, Wednesday, Thursday, Friday, Saturday};
枚举成员值类型的要求
答:枚举成员的值必须是编译期间的常量,即编译器可以计算出来的常量表达式,只包括字面量、以前定义的枚举成员、声明为const的变量;
枚举成员可以是包含默认类型int在内的任何整数类型。
枚举成员的类型规范
答:枚举成员的类型规范放在枚举类型名的后面,用冒号隔开enum class Punctuation : char {Comma = ',', Exclamtion = '!', Question = '?'};
数据类型的别名的新旧指定方式及新方法对比旧方法的优势
答:旧方式typedef unsigned long long BigOnes;
新方式using BigOnes = unsigned long long;
新方式更加直观,可读性也更好,但写出具体类型能够让代码更容易理解,因此应该有节制的使用类型别名。
变量的生存期(4种)
答:变量生存多长时间取决于其存储持续时间。
生成方式 | 存储持续时间 | 称谓 | 解释 |
---|---|---|---|
在代码块中声明的非静态变量 | 动态的存储持续时间 | 自动变量、局部变量 | 从声明它的那一刻开始,到包含其声明的代码块的结尾结束,具有局部作用域或块作用域 |
使用static关键字定义的变量 | 静态的存储持续时间 | 静态变量 | 从定义的那一刻开始,到程序结束时消失 |
在运行期间分配内存的变量 | 动态的存储持续时间 | — | 从创建它们那一刻开始,到释放其内存、销毁它们时消失 |
使用thread_local关键字声明的变量 | 线程存储持续时间 | — | — |
全局变量的特点及其访问方式、优缺点
答:全局变量在所有代码块和类外部定义,具有 全局(名称空间)作用域;
在默认情况下具有静态的存储持续时间;
初始化在main()之前进行,若没有初始化,则默认初始化为0(自动变量在没有初始化时包含的是垃圾值)
要访问全局变量value,必须使用作用域解析运算符::限定它std::cout << "Global value = " << ::value << std::endl;
全局变量使用过多会占用较多内存,且会大大增加修改变量时出错的可能性,因此原则要求避免使用全局变量,但是很适合用于定义全局常量,即用const声明的全局变量。
布尔类型
答:条件为 bool 类型,bool 值只有 true 和 false,它们是 bool 类型的字面量,布尔字面量;
bool 类型若使用空{}来初始化,初始值为false;
在条件语句中使用关系运动符和逻辑运算符(&&, ||, !)可完成各种复杂的条件;
某类型中只有0的对等值会被转换为布尔值false
bool值的输出
答:bool默认显示为0或1,可以使用std::boolalpha将其显示为true或false
std::cout << std::boolalpha;
std::cout << (5>3) <<std::endl;
if语句及其嵌套
答:
if (condition){
}
else{
}
条件运算符
答:若条件为真,则c=表达式a,否则c=表达式b c = 条件? 表达式a : 表达式b;
switch语句及其类型要求
答: switch的选择表达式应为整数表达式,即整型、字符型、枚举类型等;
case值必须唯一,但不必按顺序;
选择表达式对应哪个标签值,就执行哪个case后的语句;
若选择表达式不对应任一标签值,就执行default标签后的语句;
一般每个case后的break语句都是必需的;
default标签后的break语句不是必需的,但加上它是个好习惯。
switch (选择表达式){
case 标签1:
...
break;
case 标签2:
...
break;
case 标签n:
...
break;
default:
...
break;
}
switch语句的“贯穿”现象
答:当移除了某个case后的break语句时,它下面的case标签的代码也会运行,这叫作贯穿;
C++17为故意使用贯穿添加了新的语言功能,将原本的break语句替换为[[fallthrough]];
switch中case标签后的多条语句什么时候需要加花括号
答:1)case后的多条语句一般不需要加花括号;
2)因为花括号内的代码构成代码块,是自动变量或局部变量的生存范围。因此当需要使用局部变量时,就需要加上花括号。见下一条。
switch语句块中各个位置的变量的作用域
答:switch语句内的变量是自动变量;
其中自动变量的定义要能保证在正常执行过程中可以被访问;
因为是自动变量,所以从它的定义到整个switch语句的结束都是它的作用域,不能绕过变量的定义而进入其作用域,因此,case中若要进行变量的定义,就要加花括号,形成这个case自己的语句块;
对于放在最后的case,比如default,由于其后没有其他case,所以肯定不能绕过,因此可以进行变量的定义而不加花括号。
C++17中,为if语句和switch语句添加了初始化语句,以将变量限制到if或switch语句块:
if(initialization; condition) {...}
switch(initalization; condition) {...}
usigned int height[6] {24,34,73};
for(初始化;条件;迭代) {循环体}
。注意该函数为C++17引入,所以需设置C++标准int value[] {1,2,3,1,2,34,23,4,5,234,23,22};
for (size_t i {}; i
for (range_declaration : range_expression)
loop statement or block;
while(条件) { }
do{ }while(条件);
char name[] {"Mae West"};
std::cout << name << std::endl;
#include
const int max_length {100};
char text[max_length] {};
std::cin.getline(text, max_length, '所指定的标志输入结束的字符(默认为'\n')');
double carrots[3][4] { //定义了一个大小为3的数组,该数组的每个元素都是含有4个double类型数值的数组,数组名与起始地址为carrots
{2.5, 3.2 },
{4.3 },
{5.3, 2.5, 6.4 }
};
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; //内部每行的花括号是可选的
char stars[][80] {
"Robert Redford",
"Hopalong Cassidy",
"Lassie",
};
#include
std::array<double, 100> values;
//std::array values {0.3, 0.45, 0.23, 0.5};
values.fill(0.5);
double total {};
for (size_t i {}; i<= values.size(); ++i) {
total += values.at(i); //使用at()访问array<>对象的元素,会自动检查索引值的有效性
}
value[4] = value[3] + 2.0*values[2]; //使用[]不会进行检查
#include
std::vector<double> values;
values.push_back(0.5);
std::vector<double> values1 (20); //20个元素被初始化为0
std::vector<long> values2 (20, 99L); //20个元素被初始化为99L
std::vector<unsigned int> values3 {1, 2, 3, 4, 5}; //使用初始化列表进行初始化
const char* pproverb {"A miss is as good as a mile."};
const char* pstars[] {
"Robert Redford",
"Hopalong Cassidy",
"Lassie",
};
类型 | 注释 | 示例 |
---|---|---|
常量指针 | 指针变量中的地址不能修改 | int data {20}; int* const pdata {&data}; (const修饰指针,指针为常量,指向int) |
指向常量的指针 | 指针指向的内容不能修改 | const int value {20}; const int* pvalue {&value}; (指针指向const int) |
指向常量的常量指针 | 指针变量中的地址与指针指向的内容都不能修改 | const float value {3.14}; const float* const pvalue {&value}; |
double values[] {12.234, 28.243, 32.3, 4.54, 545, 6.445};
double* pvalue {values};
*(pvalue +1) = *(pvalue +2);
double data[] {};
for (size_t i {}; i< std::size(data); ++i) {
*(data + i) = 2 * (i+1);
}
int *p[4];
[]
比*
优先级更高,说明p
为一个数组,其中元素为int*
(指向int)类型int (*p)[4]; //n是一行里有几个元素,也就是列数
()
优先级高,首先说明p
是一个指针,指向一个整型的一维数组,这个一维数组的长度是n
,也可以说是p
的步长。也就是说执行p+1
时,p
要跨过n
列的长度。//分配内存
1) double* pvalue {};
pvalue = new double; //new 返回新分配内存的地址
2) //double* pvalue {new double {3.14} };
3) double* data {new double[100] {} }; //对数组进行动态内存分配时,编译器无法推断数组的维数,因此应显式指定数组的大小
//释放内存
delete pvalue;
pvalue = nullptr; //释放内存后,原指针成为悬挂指针,应重新设置该指针或置为空指针
delete[] data; //释放数组内存,要加上[],且这里不能填入维数
data = nullptr;
int ia[3][4] {0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11};
int (*p)[4] = ia;
std::cout << p <<std::endl; //输出数组ia起始地址, 即ia[0][0]地址
std::cout << *p <<std::endl; //输出数组ia[0]起始地址, 即ia[0][0]地址
std::cout << *(p+1) <<std::endl; //输出数组ia[1]起始地址, 即ia[1][0]地址
std::cout << *(*(p+1) +2) <<std::endl; //输出数组ia[1][2]的内容
size_t rows {3};
//double (*carrots)[4] {new double [rows][4] {} };
auto carrots {new double[rows][4] {} }; //C++11之后,可使用auto来代替上一句的写法
...
delete[] carrots;
carrots = nullptr;
.
,这一过程可用间接成员运算符->
代替std::vector data;
auto* pdata = &data;
//(*pdata).push_back(66);
pdata->push_back(66);
std::unique_ptr<double> pdata { std::make_unique<double>(999.0) };//make_unique为C++14引入
auto pdata { std::make_unique<double>(999.0) };
const size_t n {100};
auto pvalues { std::make_unique<double[]> (n) };
pvalues.reset(); //reset()可把任意类型的智能指针重置为指向nullptr.
std::shared_ptr<double> pdata { std::make_shared<double>(999.0) };
auto pdata { std::make_shared<double>(999.0) };
pdata.reset();
std::string empty; //empty为一个不包含字符的字符串
std::string proverb {"Many a mickle makes a muckle."}; //以字符串字面量定义
std::string part_literal {"Least said soonest mended.", 5}; //以字符串字面量的前5个字符定义
std::string sleeping (6, 'z'); //以6个重复字符z来定义
std::string sentence {proverb}; //以已有的string对象包含的字符串字面量来初始化
std::string phrase {proverb, 0, 13}; //以string对象从索引0开始的13个字符来初始化
return;
,或不写return语句。(double array[10])
,在实际传入的数组长度小于10时,程序会读取超过数组边界的值,因此也不能在数组参数中指定维数和索引大小。//指针
double changeIt(double* pointer_to_it);
double it {5.0};
double result {changeIt(&it)};
//数组
double average(double array[], size_t count); //使用数组表示法
double average(double* array, size_t count); //使用指针表示法。编译器认为这两个函数原型完全相同
double values[] {0.2, 1.2, 2.45, 3.78, 4.1, 5.3};
std::cout << "Average = " << average(values, std::size(values) ) << std::endl;
array[i][j]
,指针表示*(*(array+i)+j)
double yield(const double values[][4], size_t n);
double beans[3][4]{
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0 };
std::cout << "Yield = " << yield(beans, std::size(beans)) << std::endl;
do_it(it);
,不看其定义或声明就不知道函数实参是按值传送还是按引用传送,也不知道是否会修改it的值。double average10(const double (&array)[10]) { //按引用传送数组可以指定数组的第一维大小
double sum {};
for (size_t i {}; i < 10; ++i) {
sum += array[i];
return sum/10;
}
double values[] {1.0, 2.0, 3.0};
std::cout << "Average = " << average10(values) << std::endl; //此时编译器会检测传入数组的长度,传入的数组与需要的数组长度不同时会报错
print_it()
之前,会在内存的某个位置隐式地创建一个临时的double,以存储转换后的int值,然后把临时内存位置的引用传送给print_it()
。double_it(i);
后,会存在1个值为246.0的临时double变量和值仍为123的int变量i
,在继续的操作要将double类型的246.0转换到int赋给变量i
,而这是不允许的。)void double_it(double& it) { it *= 2; }
void print_it(const double& it) { std::cout << it << std::endl; }
int i {123};
//double_it(i); /*error, does not compile! */
print_it(i);
const std::string&
类型传递参数无法完全避免函数不复制输入的字符串参数std::string_view
类型(C++17标准库新增的string_view头文件中定义的一个类型)。void show_error(string_view message = "Program Error");
int main (int argc, char* argv[]) { }
re
、*re
或re.value()
来取出值;通常联合std::nullopt
和std::optional::value_or(default_value)
一起使用。void do_it(std::string number);
void do_it(std::string& number); //type与type&,不能区分
//-----------------------------------------
long larger(long a, long b); //对于按值引用,函数是不会改变实参值的,因此编译器会忽略按值引用中基本类型的const声明
long larger(const long a, const long b); //因此,对于基本类型,有无const,不能区分
//-----------------------------------------
long* larger(long* a, long* b);
long* larger( long* const a, long* const b); //同上,指针变量中所存储的地址也不会改变,因此,指针是否声明为const,无法区分
//-----------------------------------------
int largest(int* pvalues, size_t count);
int largest(float* pvalues, size_t count); //指向不同类型的指针,可区分
//----------------------------------------
long* larger( long* a, long* b);
const long* larger( const long* a, const long* b); //对指向的值加const,可禁止修改该地址中的值。指向的值是否为const,可区分。
//----------------------------------------
long& larger(long& a, long& b); //引用是对一个确定的变量的别名,不可更改,这一层面上相当于已经是常量,因此不能在&后加const
long larger(const long& a, const long& b); //但同上一条一样,T& 和 constT& 是不同的,可区分。
template <typename T> //关键字template将这段代码标识为模板;typename将其后的参数列表(本例只有一个T)中的模板参数标识为类型,
T larger(T a, T b){ //T为模板参数,作为类型的占位符,可用在具体类型的任何上下文中(函数签名、返回类型和函数体的任何位置)
return a>b ? a : b;
}
std::cout<<larger(1.5, 2.5)<<std::endl;
larger<double>(20, 19.6);
template <>
int* larger<int*> (int* a, int* b) {
return *a > *b ? a : b;
}
int* larger(int* a, int* b){
return *a > *b ? a : b;
}
template <typename T>
T larger(const T data[], size_t count){
T result {data[0]};
for (size_t i {1}; i < count; ++i)
if (data[i] > result) result = data[i];
return result;
}
template<typename T>
T* larger(T* a, T* b) {
return *a > *b ? a : b;
}
template <typename TReturn, typename TArg1, typename TArg2>
TReturn larger(TArg1 a, TArg2 b){
return a > b ? a : b;
}
larger<size_t, double>(1.5, 2); //将返回类型指定为size_t,第二个类型TArg1指定为double
template <typename T1, typename T2>
auto larger(T1 a, T2 b){
return a > b ? a : b;
}
(2) 使用decltype的拖尾返回类型语法template <typename T1, typename T2> //decltype(expression)可得到expression计算结果的类型,decltype并不会实际计算expression
//decltype(a > b ? a : b) larger(T1 a, T2 b){ //编译不会通过。因为编译器是从左往右处理模板,因此当decltype处理返回类型时,编译器还不知道a,b的类型
auto larger(T1 a, T2 b) -> decltype(a > b ? a : b) //使用拖尾返回类型语法,可将返回类型规范放到参数列表后面,此处的auto用于告知编译器返回类型规范将出现在函数头最后。
return a > b ? a : b;
}
(3) decltype(auto),该语法从C++14引入,用于简略(2)的语法。template <typename T1, typename T2>
decltype(auto) larger(T1 a, T2 b){
return a > b ? a : b;
}
上面,拖尾decltype()和decltype(auto)会推断为引用类型,且会保留const修饰符。而auto问题推断为值类型,即会不可避免地复制值template<typename TReturn=double, typename TArg1, typename TArg2> //在实参列表一开始指定默认值
TReturn larger(const TArg1& ,const TArg2&);
//------------------------------
template< typename TArg, typename TReturn=TArg> //使用一个模板参数作为另一个参数的默认值
TReturn larger(const TArg& ,const TArg&);
template<int lower, int upper, typename T>
bool is_in_range(const T&value){
return (value <= upper) && (value >= lower);
}
std::cout << is_in_range<0, 500>(value);
这种方法只能在编译期间提供上下限,但更好的方法是为上下限使用函数参数template <typename T, size_t N>
T average(const T (&array)[N]){
T sum{};
for (size_t i {}; i < N; ++i)
sum += array[i];
return sum / N;
}
//---------------------
double moreDouble[] {1.0, 2.0, 3.0, 4.0};
std::cout << average(moreDoubles) << std::endl; //在没有显式指定数组维数时,函数也能工作
//---------------------
//double* pointer = moreDoubles;
//std::cout << average(pointer) << std::endl; //编译不会通过。编译器无法从指针推断数组大小
//---------------------
std::cout << average({1.0, 2.0, 3.0, 4.0};) << std::endl; //重用了上次的模板实例
C/C++编译流程
答:C语言编译过程详解
① 预处理(Preprocessing):用于将所有的#include头文件以及宏定义替换成其真正的内容。
$ g++ -E test1.cpp -o test1.i
$ g++ -E test2.cpp -o test2.i
② 编译(Compilation):这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
$ g++ -S test1.i -o test1.s
$ g++ -S test2.i -o test2.s
③ 汇编(Assemble):将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。
$ g++ -c test1.s -o test1.o
$ g++ -c test2.s -o test2.o
④ 链接(Linking):链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
$ g++ test1.o test2.o -o test.out
g++的命令选项
答: 参考g++入门教程、man g++
gcc [-c|-S|-E] [-std=standard] //-E 预处理;-S编译生成汇编代码;-c汇编生成目标文件;-std指定语言版本
[-g] [-pg] [-Olevel] //-g编译时生成调试信息;
[-Wwarn...] [-pedantic]
[-I incdir...] [-L libdir...] //-I 设置头文件目录;-L 设置库文件目录
[-Dmacro[=defn]...] [-Umacro] //
[-foption...] [-mmachine-option...] //
[-o outfile] [@file] infile... //-o 指定输出文件名;@file 用于从文件中读取命令行选项;infile...为输入文件列表
------------------------------------------------
#对于`#include "file"`,若使用`-I`指定了头文件目录`incdir`,gcc/g++会先在`incdir`下查找头文件`file`,否则在当前目录下查找,若未找到,再到系统默认的头文件目录下查找。
#对于`#include `,若使用`-I`指定了头文件目录`incdir`,gcc/g++会先在`incdir`下查找头文件`file`,否则直接到系统默认的头文件目录查找。
#选项`-include [file]` 相当于代码中的`#include`,用于包含某个代码。
#举例:
$ gcc -std=c++17 -g -I ./include -L ./lib -include /usr/include/pianopan.h -o hello1.cpp hello2.cpp
头文件 ( .h )、静态库 ( .lib , .a ) 和共享库 ( .dll , .so )
答:GCC and Make Compiling, Linking and Building C/C++ Applications
①静态库 vs. 动态库
库是预编译目标文件的集合,可以通过链接器链接到您的程序中。示例是系统函数,例如 printf() 和 sqrt()。有两种类型的外部库:静态库和共享库:
静态库(static library) 在 Unix 中的文件扩展名为".a" (archive file),在 Windows 中的文件扩展名为".lib"(library)。当您的程序链接到静态库时,程序中使用的外部函数的机器代码将复制到可执行文件中。
共享库(shared library) 在 Unix 中的文件扩展名为".so" (shared objects) ,在 Windows 的文件扩展名为".dll"(dynamic link library) 。当您的程序链接到共享库时,只会在可执行文件中创建一个小表。在可执行文件开始运行之前,操作系统会加载外部函数所需的机器代码——这个过程称为动态链接。动态链接使可执行文件更小并节省磁盘空间,因为一个库的一个副本可以在多个程序之间共享。此外,大多数操作系统允许所有正在运行的程序使用内存中共享库的同一副本,从而节省内存。升级共享库代码无需重新编译程序。
由于动态链接的优势,默认情况下,GCC 会链接到可用的共享库。
②搜索头文件和库(-I, -L and -l)
编译程序时,编译器需要头文件来编译源代码;链接器需要库来解析来自其他目标文件或库的外部引用。
对于源代码中使用的每个头文件(通过 #include 指令),编译器会在所谓的包含路径中搜索这些头文件。包含路径是通过 -Idir
选项(或环境变量 CPATH)指定的。由于头文件名是已知的(例如,iostream.h、stdio.h),编译器只需要目录。
链接器在库路径中搜索将程序链接到可执行文件时所需的库。库路径通过 -Ldir
选项(大写"L"后跟目录路径)(或环境变量 LIBRARY_PATH)指定。此外,您还必须指定库名称。在 Unix 中,库 libxxx.a 是通过 -lxxx 选项指定的)小写字母"l",没有前缀"lib"和".a"扩展名)。在 Windows 中,提供全名,例如 -lxxx.lib。链接器需要知道目录和库名称。因此,需要指定两个选项。
转换单元
答:每个源文件及其所包含的头文件内容称为一个转换单元,即对应一个源文件和若干个头文件;
编译器独立处理程序中的每个转换单元来生成对象文件,对象文件包含机器码和实体引用的信息;
链接程序在对象文件之间建立必要的连接, 以生成可执行程序模块;
编译和链接转换单元合称为“转换”。
单一定义(ODR)规则
答:任何变量、函数、类类型、枚举类型、概念 (C++20 起)或模板,在每个转换单元中都只允许有一个定义(其中部分可以有多个声明,但只允许有一个定义)。
在整个程序(包括所有的标准或用户定义的程序库)中,被 ODR 式使用(odr-used意味着在其定义必须存在的环境中使用某些东西(variables或函数))的非 inline 函数或变量只允许有且仅有一个定义。
inline 函数或 inline 变量 (C++17 起)的定义必须在调用它们的每个转换单元中出现一次,但在所有的转换单元中,给定内联函数和变量的所有定义必须相同。因此应在头文件中定义内联函数和变量,并在需要内联函数或变量的源文件中包含这个头文件。
通常要在多个转换单元中使用类或枚举类型,故允许程序中的几个转换单元分别包含给定类型的定义,但这些定义必须相同。因此,可把类类型的定义放在头文件中,再使用#include
指令把头文件添加到需要类型定义的源文件中,但,在同一个转换单元中给定类型的重复定义是非法的。
链接属性
答:转换单元中的名称在编译链接过程中处理的方式由链接属性确定;
链接属性指定了由一个名称表示的实体可以在程序代码的什么地方使用;
当某个名称被用于在声明它的作用域外部访问其程序内容时,就有链接属性;
如果某个名称有链接属性,就同时有内部链接和外部链接属性;
内部链接属性表示其实体可以在同一转换单元的任何地方访问;
外部链接属性表示其实体可以在整个程序中共享和访问;
没有链接属性表示其实体只能在该名称中作用域中访问。
外部函数
答:函数名默认具有外部链接属性。
若函数没有在调用它的转换单元中定义,编译器就会为将这个调用标记为外部链接属性,让链接程序处理。
因此,对包含A函数定义的A.cpp、包含A函数声明(原型)的B.cpp,通过链接即可生成正确的可执行文件。
但通过把函数原型放到头文件中,然后使用#include
指令把头文件包含到转换单元中。
外部变量
答:(1)非const变量默认具有外部链接属性。若要访问在当前转换单元外部定义的变量,必须使用extern
关键字声明变量名称,避免违反ODR规则,也可以在上一条的外部函数声明前使用extern
修饰符,以明确指出函数定义在另一个转换单元中。
//cpp1
int power_range{3}; //定义一个全局变量
double power(doublex, int n){...} //定义一个函数
//cpp2
extern int power_range; //extern必需,全局变量在没有初始化列表时自动初始化为0,加extern避免违反ODR规则
extern double power(double x, int n); //extern可选
(2)const变量在默认情况下有内部链接属性,这使它不能在其他转换单元中使用,使用extern
关键字可以重写这个属性,
//cpp1
extern const int power_range{3}; //定义一个全局常量,注意对const变量添加外部链接属性要在定义时也加入extern
//cpp2
extern const int power_range; //声明
内部名称
答:上面是需要声明外部链接的场合,有时还会有一些只需要在当前转换单元使用的局部辅助函数,但“函数名默认具有外部链接属性”的特性使得它们总会有外部链接属性,也就不能在其他转换单元中定义有相同签名的函数。过去的解决方案是使用static
关键字声明函数。在现代C++中:
任何时候都不要再使用static
关键字来标记应该具有内部链接属性的名称;相反,应该总是使用未命名的名称空间。
预处理指令
答:#
字符串化运算符;##
连接运算符
#define IDENTIFIER sequence of characters
该指令将宏标识符IDENTIFIER
替换为一个字符串,以前常用于定义符号常量、创建类似于函数的宏。但在现代C++中:
总不要使用预处理宏来定义符号常量和简单函数,而总应该使用const常量和普通的C++函数或函数模板
头文件
答:在源文件中包含自己的头文件时,常用双引号:#include "myheader.h"
;
多个或多级头文件包含时,有些头文件可能会被多次包含到一个源文件中;#include
指令的无限递归会导致编译失败。可使用下面的 #include保护符:
//对于头文件myheader.h
#ifndef MYHEADER_H
#define MYHEADER_h
//myheader.h中的所有代码放在这里
#endif
名称空间
答:如果没有定义名称空间,就默认使用全局名称空间;
当存在同名的局部声明覆盖了全局名称时,就需要使用作用域解析运算符显式地访问全局名称:::power(2.0, 3)
;
在名称空间中不能包含main()
函数;
如果要把函数放在名称空间中,只需要把函数的原型放在名称空间中即可,函数可以在其他地方定义,但要使用限定过的名称。
当名称空间较长时,可以为其定义一个别名:
namespace alias_name = original_namespace_name;
namespace calc{
...
}
namespace sort{
...
}
namespace calc{ //扩展名称空间定义
...
}
未命名的名称空间
答:在不给名称空间指定名称时,会由编译器生成一个内部名称;
在一个转换单元中只能有一个未命名的名称空间,其余没有命名的名称空间都是其扩展;
未命名的名称空间中声明的所有名称都具有内部链接属性(即使用extern修饰符定义了名称),它们的实体都是定义它们的转换单元中的局部成员。
逻辑预处理指令
答:逻辑#if指令:可以定义预处理标识符,为要编译的代码指定环境,并据此选择代码或#include指令。在不同的硬件或操作系统环境中运行或维护应用程序时非常有用。
#if constant_expression
//
#endif
# if LANGUAGE==EN
//
#elif LANGUAGE==CN
//
#else
//
#endif
调试方法
答:(1)在函数中使用预处理指令#if #endif,以隔离调试所需的代码;
(2)使用assert()宏。assert(expression)
会在expression
false时,终止程序,并输出诊断信息。其中的expression可以是任意逻辑表达式。assert()的头文件是
,通过在cassert的#include语句前添加#define NDEBUG
可关闭预处理断言机制,忽略转换单元中所有断言语句。
(3)静态断言:
静态断言是语言内置的部分,用于在编译时静态检查条件;
当constant_expression是false时,程序会编译失败,若提供了error_message,就会输出一条包含它的诊断消息,否则编译器将基于constant_expression生成一个;当constant_expression为true时,静态断言什么也不会做;
静态断言的一个常见用途是在模板定义中验证模板参数的特征。
static_assert(constant_expression, error_message);
static_assert(constant_expression); //C++17新加,省略error_message字符串字面量
//--------------------------
static_assert(std::is_arithmetic_v(T), "Type parameter for average() must be arithmetic");
面向对对象编程(OOP)
答:根据要解决的问题范围内所涉及的对象来编写程序,因此程序开发过程的一部分是设计一组类型来满足这个要求。
封装
答:给定类的每个对象都组合了下述内容:
一组数据值,作为类的成员变量,指定对象的属性;
一组操作,作为类的成员函数。
把这些数据值和函数打包到一个对象中,就称为封装;
在一般情况下,不允许访问对象的数据值,这就是数据隐藏,数据成员一般不能从外部访问;
隐藏对象中的数据,可以禁止直接访问该数据,但可以通过对象的成员函数来访问;
成员变量表示对象的状态,操纵它们的成员函数则表示对象与外界的接口;
在设计阶段,正确设计类的接口非常重要,以后可以修改其实现方式,而不需要对使用类的程序进行任何修改;
建议:以一致的方式隐藏对象的数据,并只通过其接口中的函数来访问和操作对象的数据。
继承
答:根据类BankAccount
定义一个新的LoanAccount
叫做继承,BankAccount
被称为基类,LoanAccount
派生于BankAccount
。
多态性
答:多态性表示在不同的时刻有不同的形态。它意味着属于一组继承性相关的类
的对象可以通过基类指针和引用来传送和操作。指向基类的指针变量可以存储该基类及其派生类对象的地址,这组类中会有相同的基类中的成员函数,该指针对其中某个成员函数名的调用会根据所指向对象的不同而调用不同的函数实体,即同一个函数调用会根据指针指向的对象完成不同的操作。
类成员访问修饰符
答:private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问。
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数
定义类
//Box.h
#ifndef BOX_H
#define BOX_H
class Box{
private:
double length {1.0};
double width {1.0};
double height {1.0};
public:
//Box (double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);//有初始值的默认构造函数
Box(double length, double width, double height);
//explicit Box(double size);
Box(const Box& box);
Box() = default; //显式生成默认构造函数(在需要无参数的默认构造函数时,推荐)
//Box() {} //或自己定义默认构造函数(不推荐)
double volume();
}; //类定义的右花括号后面必须有分号
#endif
//------------------------------------------
//Box.cpp
#include "Box.h"
#include
//Constructor define
//成员初始化列表对成员变量赋值
Box::Box(double lv, double wv, double hv) : length{lv}, width{wv}, height{hv} { } //最为推荐,注意初始化列表放在定义时
//Box::Box(double side) : Box{side, side, side} { } //委托构造函数
Box::Box(const Box& box) : Box(box.length, box.width, box.height} {} //副本构造函数、委托构造函数
double Box::volume(){
return length*width*height;
}
//------------------------------------------
//main.cpp
#include
#include"Box.h"
int main(){
Box firstBox {80.0, 50.0, 40.0}; //使用自定义构造函数创建对象
double firstBoxVolume {firstBox.volume()};
Box secondBox; //使用默认构造函数创建对象
double secondBoxVolume {secondBox.volume()};
}
```
构造函数
答:构造函数用于在创建对象时设置成员变量的值,确保成员变量包含有效的值;
构造函数常常与包含它的类同名;
构造函数没有返回值(所以也没有返回类型);
如果不为类定义任何构造函数,编译器将生成默认构造函数;
默认构造函数没有参数,其唯一作用就是创建对象;
若使用默认构造函数创建对象,成员变量就会使用默认值,如果没有为指针类型或基本类型的成员变量指定初始值,它们就会包含垃圾值;
只要用户提供了(任何)构造函数,编译器就不会生成默认构造函数了,若此时仍然想让对象被默认构造,可使用default关键字;
在定义类时的常见做法是将类放到一个头文件中,将成员函数和构造函数放到对应的源文件中;
可以为类的成员函数指定参数的默认值,构造函数和成员函数的默认实参值总是放在类中,不放在外部构造函数和成员函数中;
所有参数都有默认值的构造函数算作默认构造函数,即这样的构造函数会与使用default构造的默认构造函数冲突;
可以在构造函数头后添加成员初始化列表对成员变量赋值,初始化顺序由类定义中声明成员变量的顺序决定。
使用explicit关键字
答:类的构造函数只有一个参数是有问题的,如
//对Cube类,其构造函数为:
Cube(double aside);
//它的一个成员函数为:
bool hasLargerVolumeThan(Cube aCube); //该函数用于比较当前对象与作为参数的aCube对象的体积大小
//可见上面这个函数的参数应该是一个Cube类型的对象,如下面这样使用:
Cube box1 {7.0};
Cube box2 {3.0};
if (box1.hasLargerVolumeThan(box2))
std::cout<< "box1 is larger than box2" <<std:endl;
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
//但由于构造函数只有一个参数,当成员函数hasLargerVolumeThan的参数与构造函数的参数类型相同或缩窄转换后相同时,对于如下语句:
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
if (box1.hasLargerVolumeThan(5.0))
std::cout<< "box1 is larger than 5.0" <<std:endl;
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
//编译器会将实参5.0转换为一个Cube对象:
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
if (box1.hasLargerVolumeThan(Cube {5.0}))
std::cout<< "box1 is larger than 5.0" <<std:endl;
//这相当于把box1与一个边长为5.0的box进行了比较,与期望的用法不同
//把构造函数声明为explicit可避免这种情况:
expilicit Cube(double aside);
explicit声明用在(头文件)函数定义内的原型中;
编译器不会把声明为explicit的构造函数用于隐式类型转换,它只能在程序代码中显式创建对象。默认情况下,应该将所有包含一个实参的构造函数(包括有多个参数,但除了第一个参数外其他参数都有默认值的构造函数)声明为explicit。
委托构造函数
答:在初始化列表中调用同一个类中的另一个构造函数完成实例化
副本构造函数
答:通过复制已有的对象来创建对象;
若没有定义副本构造函数,编译器会提供一个默认的副本构造函数;
由于复制指针时,不会复制指针指向的内容,当成员变量中存在指针时,通过副本构造函数创建的对象就会与原对象链接起来,导致两个对象都指向相同内容的成员;
在自定义副本构造函数时,考虑以下问题:
使用副本构造函数创建新实例时,若副本构造函数是按值传递,就会创建输入对象的副本,这就又会调用副本构造函数,导致副本构造函数的无限递归调用。因此:
副本构造函数的实参必须是const引用(const是由于要保证不改变原对象)。
不用参数创建对象的方法(互相冲突):
答:default构造函数、
在构造函数中为所有参数添加默认值、
为成员变量添加默认值
为什么成员变量要用private声明
答:暴露出来的接口应该是稳定的,而类中的变量随着需求的变动有可能会增删改,private成员变量可使类的调用更加鲁棒;
对于非常稳定的成员变量,可以将它们声明为public,可方便其使用。
访问私有类成员
答:使用访问器函数(getter)来提取成员变量的值;
使用更改器成员函数(setter)来修改成员变量;
double getLength() {return length;}
void setLength(double lv} {if (lv>0) length = lv;}
this指针
答:在执行任何成员函数时,该成员函数都会自动包含一个隐藏的指针,称为this指针,该指针包含调用该成员函数的对象的地址;
一般情况下不必显式使用this指针。
从函数中返回this指针的用法
答:把成员函数的返回类型指定为类类型的指针或引用,就可以从函数中返回this指针或其解引用。
Box* Box::setLength(double lv){
if (lv>0) length = lv;
return this;
}
Box* Box::setWidth(double wv){
if (wv>0) width = wv;
return this;
}
Box* Box::setHeight(double hv){
if (hv>0) height = hv;
return this;
}
Box mybox{3.0, 4.0, 5.0};
mybox.setLength(-20.0)->setWidth(40.0)->setHeight(10.0); //使用指针的方法链
//----------------------------------------------
//----------------------------------------------
Box& Box::setLength(double lv){
if (lv>0) length = lv;
return *this;
}
Box& Box::setWidth(double wv){
if (wv>0) width = wv;
return *this;
}
Box& Box::setHeight(double hv){
if (hv>0) height = hv;
return *this;
}
Box mybox{3.0, 4.0, 5.0};
mybox.setLength(-20.0).setWidth(40.0).setHeight(10.0); //使用引用的方法链
const对象和const成员函数
答:类类型的const变量称为const对象,构成const对象状态的任何成员变量都不能被修改;
这一原则也适用于指向const变量的指针和对const变量的引用;
对于const对象,只能调用const成员函数,即要把所有不修改对象的函数指定为const;
在const成员函数内不能调用任何非const成员函数;
有无const是函数签名的一部分,因此可以用const版本来重载一个非const版本的成员函数;
添加public成员函数来返回对private成员变量的引用,越过了成员变量的private声明,可以在类外读写private成员变量,但这与简单地将这些变量声明为public一样不好;
const成员函数隐含的this指针类型是其对象的const指针,即不能修改对象;
可以使用mutable关键字声明成员变量,以在const对象中修改它;
cnost Box mybox {3.0, 4.0, 5.0}; //const对象
Box mybox {3.0, 4.0, 5.0};
const Box* boxpointer = &mybox; //指向const变量的指针
const Box& boxrefer = mybox; //对const变量的引用
//----------------------------
double volume() const; //把不修改对象的函数指定为const
double Box:volume() const {return length*width*height;}
//---------------------------
double& length() { return _length; } //public成员函数,返回private成员变量的引用(不推荐)
double length() const { return _length; } // const重载
友元
答:友元可以访问类对象的任意成员,无论这些成员的访问修饰符是什么(只有在绝对有必要时才应使用友元);
类的友元函数要在类定义中用关键字friend来编写函数原型,友元函数可以是一个全局函数或另一个类的成员。
友元函数不是类成员,所以成员变量必须用对象名来限定;
友元类的所有成员函数都可以不受限制地访问原有类的成员;
friend double surfaceArea(const Box& abox);
friend class Carton;
类的对象数组
答:类对象的数组的每个元素都根据初始化参数由构造函数创建。
类对象的大小
答:类对象的大小一般是类中成员变量大小的总和,或稍大(这是由于边界对齐导致的)
类的静态成员
答:静态成员是指声明为static的成员;
静态成员独立于类类型的所有对象,但可由它们访问;
静态成员变量只定义一次,无论定义多少个类对象,每个静态成员变量的实例只有一个;
即使没有创建对象,静态成员变量也存在,可以使用类名限定变量名来使用静态变量;
同时将静态成员变量声明为inline变量,可在头文件中初始化它们,而不必在源文件中单独进行定义,避免违反ODR;
静态成员变量常常用于定义常量,可避免每个对象都创建一个该成员变量的副本;
通常将定义为static和const的所有成员变量也定义为inline,以在类定义中直接初始化;
对关键字static、inline、const的顺序没有要求;
可以定义类类型的静态成员变量,但需要在类的外部进行定义和初始化;
静态成员函数也独立于类的对象,可使用类名调用静态函数;
由于静态成员函数是所有对象公用的,因此可以通过对象来调用静态成员函数(不推荐),但静态成员函数不能访问调用它的对象;
若想让静态成员函数访问类的对象,需要把该对象作为参数传递给静态函数;
由于静态成员函数与对象无关,所以它没有this指针,不能使用const声明。
析构函数
答:对类对象应用delete运算符或处在创建类对象的块的末尾,就会释放类对象;
释放类对象会执行析构函数,它与类同名,但名称前有一个~
;
如果没有定义析构函数,编译器会提供一个默认的;
~Box() {} //声明
Box::~Box() = default; //使用default定义一个默认析构函数
使用指针作为类成员
答:现实中的程序通常由大量彼此协作的对象构成,这些对象通过指针、智能指针和引用链接在一起;
需要创建这些对象的网络,将它们链接在一起,最后再释放它们(通常使用智能指针)。
嵌套类
答:在嵌套类内可以访问外层类的私有成员。
type | 功能 | 描述 |
---|---|---|
std::vector<> 封装动态大小数组的序列容器。 |
.reserve(size_type n) | 为向量申请内存空间 |
.at(size_type n) | 返回对位置元素的引用 | |
.erase(iterator) | //删除一个元素 vector::erase(iterator position); //删除一个范围内的元素 vector::erase(iterator start_position, iterator end_position); 它返回一个迭代器,指向由 vector::erase() 函数擦除的最后一个元素后跟的元素。 |
|
std::unordered_map |
.end() | 返回一个迭代器,该迭代器指向unordered_map容器中容器中最后一个元素之后的位置 |
.find(key) | 如果给定的键存在于unordered_map中,则它向该元素返回一个迭代器,否则返回映射迭代器的末尾,因此可配合 end()判断某键值对是否在map中 | |
.at(key) | 返回对应元素value的引用 | |
.count(key) | 检查unordered_map中是否存在具有给定键的元素,如果Map中存在具有给定键的值,则此函数返回1,否则返回0。 | |
.emplace() | 向容器中添加新键值对,效率比 insert() 方法高。 | |
.empty() | 若容器为空,则返回 true;否则 false。 | |
.size() | 返回当前容器中存有键值对的个数。 | |
std::set 按照特定顺序存储; 关联容器,key就是value,key唯一; |
.insert() | 在集合容器中插入元素 |
答:
Eigen内存分配器Eigen::aligned_allocator
在使用Eigen的时候,如果STL容器中的元素是Eigen数据库结构,比如下面用vector容器存储Eigen::Matrix4f类型或用map存储Eigen::Vector4f数据类型时:
std::vector<Eigen::Matrix4d>;
std::map<int, Eigen::Vector4f>;
这么使用编译能通过,但运行时会报段错误。
对eigen中的固定大小的类使用STL容器的时候,如果直接使用会出错,所谓固定大小(fixed-size)的类是指在编译过程中就已经分配好内存空间的类,为了提高运算速度,对于SSE或者AltiVec指令集,向量化必须要求向量是以16字节即128bit对齐的方式分配内存空间,所以针对这个问题,容器需要使用eigen自己定义的内存分配器,即aligned_allocator。
这个分配器所在头文件为:
#include
根据STL容器的模板类,比如vector的声明:
template<typename _Tp, typename _Alloc = allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc> {
.....
}
使用aligned_alloctor分配器,上面的例子正确写法为:
//std::vector;
//std::map;
std::vector<Eigen::Matrix4d,Eigen::aligned_allocator<Eigen::Matrix4d>>;
std::map<int, Eigen::Vector4f, Eigen::aligned_allocator<std::pair<const int, Eigen::Vector4f>>;
上述的这段代码才是标准的定义容器方法,只是我们一般情况下定义容器的元素都是C++中的类型,所以可以省略,这是因为在C++11标准中,aligned_allocator管理C++中的各种数据类型的内存方法是一样的,可以不需要着重写出来。但是在Eigen管理内存和C++11中的方法是不一样的,所以需要单独强调元素的内存分配和管理。
std::allocate_shared
用法:std::shared_ptr<类型A> 指针变量B = std::allocate_shared<类型A> (类型A的allocator, 指针变量B所指向的类型A的参数列表);
std::shared_ptr<std::pair<int,int>> baz = std::allocate_shared<std::pair<int,int>> (std::allocator<int>,30,40);
//类型Frame中含有Eigen类型的变量
std::shared_ptr<Frame> pkf = std::allocate_shared<Frame>(Eigen::aligned_allocator<Frame>(), *pcurframe_);