1.const
const
修饰变量,表示变量不可修改,这样的变量称为常量。后续统一将没有const
修饰的变量叫变量,const
修饰的变量叫常量。
1.1.修饰指针时,注意区分限定的是指针还是指针指向的对象。
int a;
const int * p1 = &a;// const修饰p1指向对象,此时,允许修改p1,不允许修改*p1。p1++合法,*p1=1非法。
int * const p2 = &a;// const修改p2自身,此时,不允许修改p2,允许修改*p2。p2++不合法,*p2=1合法。
1.2.顶层const
,底层const
1.2.1.区分
int a;
const int *p1 = &a;// 这里我们定义一个变量p1。此时p1是变量。p1指向的对象是常量。
int * const p2 = &a;// 这里我们定义一个变量p2。此时p1是常量。p1指向的对象是变量。
const int &q1 = a;// 这里我们定义一个引用q1,注意引用不是变量,只是给引用对象起来一个别名。 此时q1是变量a的引用。不允许通过q1修改a的值。
int & const q2 = a;// 用const修饰引用自身没有意义。引用自身本来就是初始化绑定变量后不可变更绑定的。允许通过q2修改a的值。
(1).顶层const
修饰变量自身。如上述修饰p2
的const
。
(2).底层const
修饰变量/引用指向对象。如上述修饰p1
,q1
的const
。
1.2.2.作用
(1).顶层const
变量执行拷贝构造,赋值时
若左边变量用顶层const
修饰,右边可以是同类型变量,也可是同类型常量。
若左边变量无顶层const
修饰,右边依然可以是同类型变量,也可是同类型常量。
class A
{
private:
int m_i;
};
int main()
{
A a;
const A ca = a; // ok
const A cb = ca; // ok
A b = ca; // ok
a = b;// ok
a = ca;// ok
}
从理解的角度,拷贝或赋值时,左边和右边是两个独立的变量。右边的变量只负责提供内容。顶级const
修饰与否,对变量内容无影响。
(2).底层const
指针类型变量执行拷贝构造,赋值时
若左边变量用底层const
修饰,右边必须是同类型的变量或常量,是否被底层const
修饰均可以。
若左边变量无底层const
修饰,右边必须是同类型的变量或常量。不可被底层const
修饰。
class A
{
private:
int m_i;
};
int main()
{
A a;
const A ca = a;
A* p1 = &a;// ok
A* p2 = &ca;// err
const A *cp1 = &ca;// ok
const A* cp2 = &a;// ok
p1 = &a;// ok
p1 = &ca;// err
cp1 = &a;// ok
cp1 = &ca;//ok
}
引用执行初始化时,
左边被底层const
修饰时,右边可以是所引用类型的变量或常量。
左边没有底层const
修饰时,右边只能是所引用类型的变量。
class A
{
private:
int m_i;
};
int main()
{
A a;
const A ca = a;
A& aa1 = a;// ok
A& aa2 = ca;// err
const A& bb1 = a;// ok
const A& bb2 = ca;// ok
}
1.3.可见性
c
语言中被const
修饰的全局变量,只在所在文件内可见。额外添加extern
修饰后,变为全局可见。
c
语言中被static
修饰的全局变量,只在所在文件内可见。
c
语言中无const,static
修饰的全局变量,全局可见。
2.强制类型转换
2.1.新式
(1).static_cast
:会导致精度损失的数值转换;void*
到其他指针类型的转换。
(2).const_cast
:用于为右边对象去掉或加上底层const
属性。
(3).reinterpret_cast
:非void*
指针类型间转换。
(4).dynamic_cast
:用于基类指针,基类引用转换为派生类指针,派生类引用。
2.2.旧式
旧式(TargetType)xx
;
3.重载与作用域
同一个作用域内各个同名函数间有重载关系;
名字查找先从内层作用域依次往外层作用域查找;在当前作用域内找到名字后,停止继续查找。进入匹配阶段。
一个实例:
#include <iostream>
using namespace std;
struct Base{
void f(double i){cout << "Base:" << i << endl;}
};
struct Derived : public Base{
using Base::f;
void f(int i){cout << "Derived:" << i << endl;}
};
int main(){
Base b;
b.f(4.5);
Derived d;
d.f(4.5);
d.f(10);
return 0;
}
上述执行d.f(4.5)
匹配到了基类版本。这是因为我们通过在派生类里执行using Base::f;
使得基类的符号f
参与到从派生类作用域开始的名字查找过程。这样,执行d.f(4.5)
时,通过在派生类作用域的名字查找找到两个为f
的名字。接下来通过类型匹配绝对采用哪个f
。这样,由于基类的f可以精确匹配,所以,最终选择基类版本。
#include <iostream>
using namespace std;
struct Base{
void f(double i){cout << "Base:" << i << endl;}
};
struct Derived : public Base{
//using Base::f;
void f(int i){cout << "Derived:" << i << endl;}
};
int main(){
Base b;
b.f(4.5);
Derived d;
d.f(4.5);
d.f(10);
return 0;
}
上述执行d.f(4.5)
匹配到了派生类版本。因为,执行d.f(4.5)
时,通过在派生类作用域的名字查找只能找到一个为f
的名字。所以,最终选择派生类版本。针对派生类执行调用时,首先在派生类搜索函数名称,若搜索到,进入匹配阶段;若搜索不到,继续到基类搜索函数名称,若搜索到,进入匹配阶段;若搜索不到,报错。
这里,额外注意两点:
(1).名字查找和类型匹配发生在编译阶段。
(2).using
改变的是关联符号在作用域内的可见性。上述using
使得Base::f
在派生类作用域可见。默认下,Base::f
在基类作用域可见。
4.默认实参。
用作默认实参的名字在函数声明所在的作用域内被解析,而这些名字的求值过程发生在函数调用时。
内联函数在编译时展开,展开函数需要定义。
5.函数匹配
5.1.确定候选函数和可行函数
候选函数:
(1).其声明在调用点可见。
(2).与被调用函数同名。
可行函数:
(1).形参数量与实参数量相等。
(2).每个实参类型和对应形参类型相同或可转换成形参类型。
5.2.寻找最佳匹配
(1).该函数的每个实参的匹配都不劣与其他可行函数需要的匹配
(2).至少有一个实参的匹配优于其他函数提供的匹配。
5.3.实参类型转换等级:
(1).精确匹配:
a.类型相同。
b.数组或函数转成指针。
c.去掉或添加顶层const
。
(2).非底层const
实参转成底层const
形参。
(3).类型提升,派生类指针转成基类指针。
(4).类类型转换
同一等级不再区分优劣。