page34 无符号数相减
unsigned u = 10 , uu = 42;
cout <<u-uu<< endl;
cout<<(unsigned)-32<<endl;
cout<<pow(2,32)-32<<endl;
return 0;
page41 初始化发生了什么?
struct A{
int i = 0;
int j = 0;
A() = default;
A(int k){i = k ; cout<<"k"<<endl; };
A(A& b){cout<<"test2"<<endl;};
A& operator = (const A& b){cout<<"kaobeifuzhi"<<endl;};
};
int main()
{
A a ;
A b ;
A c = a;//输出:test2
A d={16}; //输出:k
return 0;
}
page 45
声明语句组成:一个基本数据类型+声明符号列表
声明符命名一个变量,并指定改变量为与基本数据类型相关的某种类型
page53
指针的引用
int* p1 , p2 //p1是指针。p2是整型
int *&r;//从右到左结合:&r表明r是个引用,int *&r表明该引用所引用的类型是一个指向整型对象的指针
page54 为什么const限定的对象一定要有初始值
编译器会把const对象换成对应的值,因此,每个文件都必须提供const对象的初始值。在默认情况下,const对象仅仅在一个文件内有效,即我们可以在一个项目的不同文件中定义同名的const对象。
如果想很多文件共享同一个const对象,在不管是声明还是定义,在前面加上extern语句。如果有初值,则即使声明又是定义。没有初值的话,则是声明。
例如,文件a.cpp内:
extern const int a = 250;
int b = 90 ;
文件b.cpp内:
#include <iostream>
using namespace std;
extern const int a ;
extern int b ;
int main()
{
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
命令行
g++ a.cpp b.cpp -o c
但是,如果在a.cpp中,定义a的时候没有extern关键字,则程序将会报错,这是因为const默认情况下只在文件内有效。
page59 constexpr
常量表达式满足两点: 1.值不会改变;2.编译时就会得到结果
例如 int i = 8 ;不是常量表达式,因为i是个变量。
我们知道,const限定的对象,在编译时会将等号右边的值替换它,因此,有如下代码和注释:
int i = 9 ;
const int j = 10 ; //j是常量表达式
const int k = i ; //k不是常量表达式
constexpr int s = j+1 //正确:j是常量表达式,所以j+1也是常量表达式
constexpr int q = k+1//错误:k不是常量表达式,因此k+1不是常量表达式
常量表达式的优点是显然的,在编译的时候就已经把它计算出来了,每当程序运行的时候,就不用再去计算它。
C++11之所以引入constexpr限定符,是因为有些复杂系统,我们无法知道它是否是常量表达式,加上该限定符后,就是告诉编译器,如果等号左边是常量表达式,那么就执行,否则,就报错。
常量表达式得到的数据类型叫做字面值类型,指针和引用都可以定义为字面值类型,但注意,字面值类型必须是编译的时候就能计算出来,因此指针和引用所指向的对象必须有固定的地址,或者指针的值是nullptr.只有定义所有函数体之外的变量具有固定地址,因此引用和指针不能指向任何函数体之内的对象。
当然这有例外,如果函数体内可以定义有效范围超出函数体本身的变量,它变可能有固定地址。这种变量叫做 局部静态对象,它在程序第一次执行它时候初始化,直到程序终止时候才被释放。例如下面的语句是正确的:
void test(int i = 0 ){
static int a = i;
constexpr int &b = a ;
}
page 60 typedef
代码
typedef int (*P)[10] ;
P p;
cout<<sizeof(*p)<<endl;
cout<<10*sizeof(int)<<endl;
page 63 decltype 和 引用
decltype可以返回引用,因此:
int i = 0 ;
int &r = i ;
decltype(r) rr ;//出错:r是引用类型
指针的解引用运算符返回引用类型,因此
int i = 0 ;
int *p = &i ;
decltype(*p) rr ;//出错:r是引用类型
当decltype内是一个变量时候,它返回该变量的类型。但如果是改变量外还有个括号,则decltype推断它是表达式。变量是可以作为赋值语句左侧的特殊表达式,因此,它将返回引用类型:
int i ;
decltype( (i) ) r ; //错误:r是引用类型,需要初值
page 76 补充:关于隐式转换
拷贝初始化的时候,如果被初始化的对象是常量引用,那么,它可以绑定到临时对象上,这个时候,可以通过隐式转换构造临时对象:
struct A{
A() = default;
A(int k) { cout<<"test"<<endl; };
};
int main(){
const A &a = 16 ;//正确,发生隐式转换
A b = 16 ;//错误
const A c = 16 ;//错误
return 0 ;
}
这里还要强调拷贝初始化和直接初始化的区别:拷贝初始化一般调用拷贝构造函数,是把参数里的变量拷贝到所构造的对象内,必要时还要进行类型转换。直接初始化则使用函数匹配进行初始化。
page 95 注记:下面代码均不输出250.
vector<int> v = {1,2,3} ;
auto b = v.begin() , e = v.end();
v.push_back(250);
cout<<*(b+3)<<endl;
cout<<*e<<endl;
page114 多维数组和范围for循环
本质上,c++没有多维数组,所谓多维数组,是数组的数组
范围for循环中,除了最内层循环外,其它循环应该声明成引用,这是为了避免将数组转化为指针。例如:
int a[10][10][10];
for(auto &x : a )
for(auto &y : x)
for(auto &z : y )//改变数组的值,所以最内层也要声明成引用
z = 250;
for(auto &x : a )
for(auto y : x)//编译报错,y必须要声明为引用,否则,它被转化成指针,从而导致下一层循环没意义
for(auto z : y )//不改变z,该层可以不声明成引用,但明显声明成引用效率更高
cout<<z<<" "<<flush;
cout<<endl;
page120 当一个对象作为左值的时候,使用的是该对象的身份;当一个对象作为右值的时候,使用的是该对象的值。
左值不一定处在赋值号的左边,例如const对象是左值,但不能出现在赋值号的左边。
递增或者递减运算符返回左值:
++i = 9;
page174 异常
关键字 throw 用于抛出异常,用法是 throw + 异常类型对象,例如抛出一个runtime类型的异常:
throw runtime_error("test") ;
注意,runtime对象必须使用一个字符串对它初始化。
try语句可以吞下异常,例如:
try{
cout<<"please input 1: "<<endl;
int i ;
cin>>i ;
if(i != 1)
throw runtime_error("you should input 1 !") ;
}catch (runtime_error err){
cout<<"catch succeed !"<<endl ;
}
这里我们使用catch吞下了异常。
page196 如何向函数内传递多维数组??
我们知道,不能像函数传递数组,所有为数组的形参,都会转化为指针,例如,下面的函数声明是等价的:
void g(int a[250]);
void g(int* p);
void g(int* a[]);
传递一个数组,我们需要传递两个参数,指向数组首元素的指针和数组长度。
指针移动的步长是根据指针类型决定的,和指针本身的表达方式和存储方式无关,我们可以像函数内传递一个指向数组的指针。因此,例如我们要传递数组a[10][100] ,则可以这样声明函数
void f( int (p*)[100] , rows) ;
通过如下方式调用:
f(a,10);
page198 可变形参的函数
其实 ,也可以用vector实现可变形参函数:
void h(vector<int> v){
for(auto &s : v)
cout<<s<<endl;
}
调用代码:
h({1,2,3});
page205 返回数组的指针或者引用:
int (*func(int a))[10] ;
func(int a)表明func是个函数,它调用一个整型形参;
*func(int a)表明,该调用返回类型是一个指针类型;
*func(int a)[10]表明,该指针指向一个具有10个元素的数组;
int *func(int a)[10] 表明,该数组内的类型是整型。
page 208 函数重载
重载不去分顶层const,因此,如下每对的声明都是相同的
void f(const int);
void f(int);
void g(int* const );
void g(int*);
常量引用和常量指针是底层const。
page210 作用域内声明的函数,将隐藏外层作用域的同名实体(即使形参不同)。测试代码
#include <iostream>
using namespace std;
void test(int i ){
cout<<"int"<<endl;
}
void test(double j ){
cout<<"double"<<endl;
}
int test(bool b){
cout<<"bool"<<endl;
}
int main()
{
int i ;
double d;
bool b;
cout<<"before"<<endl;
test(i);
test(d);
test(b);
cout<<"after"<<endl;
void test(bool) ;
test(b);
test(i);//输出bool,而非int
test(d);//输出bool,而非double
return 0 ;
}
page 212 默认实参
我们可以只在声明中设定默认实参,然而只能设定一次。
page 214 返回类型为constexpr函数
我们允许这种函数在定义时返回非常量,当调用它的时候,编译器会判断它返回的是否是常量表达式,如果不是常量表达式,则报错
内联函数相当于在编译过程内联的展开,因此,内联函数可以在程序中重复定义。但一个源文件中只能定义一次
constexpr函数也可以重复定义。但一个源文件中只能定义一次。
注记:constexpr函数内,只能是一条return语句,不能有其它语句!!!
page230 类
成员函数调用:
a.f()
1. this指针:上述语句,隐式将a的地址传给f内的指针this。默认情况下,this是指向非常量的常量指针,因此,默认情况下它不能绑定到常量对象身上。
2.引入const:为了使this能够绑定到常量对象上,在定义成员函数的时候,引入const,代码例子:
const int& f() const {cout<<"const"<<endl;return i ;}
当然,这个函数也可以调用非常量版本的对象,但不能改变它:
//以下是类定义代码:
//....
const int& f() const {cout<<"const"<<endl;return i ;} //常量版本
//...
//以下是main函数以内代码:
//...
A a;
//a.f() = 100 ;//出错,因为this指向常量
a.f();
//...
如果我们想改变i的值,则重载它:
//以下是类定义代码:
//....
const int& f() const {cout<<"const"<<endl;return i ;} //常量版本
int& f(){cout<<"non_const"<<endl ; return i ;}
//...
<pre name="code" class="cpp">//以下是main函数以内代码:
//...
A a;
a.f() = 100 ;//正确,调用接受非常量版本
a.f();
//...
注记:编译顺序:先编译成员,再编译成员函数,因此,成员函数可以调用源文件中定义在它后面的成员。
page235 构造函数
构造函数可以对const对象写值,只有构造函数完成初始化后,const对象才具有“const”属性。
测试代码:
struct A{
A(const int& k ): i(k) {}
private:
const int i = 10;
};
注意,对i初始化语句是i(k) 。
一个疑问:
//文件a.cpp内:
#include <iostream>
using namespace std;
struct A{
int i ;
void f(){cout<<"hello"<<endl;}
};
void g();
int main()
{
A a;
a.f();
g();
return 0;
}
//文件b.cpp内:
#include <iostream>
using namespace std;
struct A{
int i ;
void f(){cout<<"hi"<<endl;}
};
void g(){
A a;
a.f();
}
shell命令:
g++ a.cpp b.cpp -o c
结果,输出都是“hello”
page242 友元
在类内声明类外函数为类的友元,使得这个函数能够访问类的私有成员
类内关于友元的声明,只是声明了该函数的访问权限,如果我们想在类外调用它,则还要声明它
声明示例:
//类内
//...
friend void fr(A a);
//...
//类外
//...
//友元函数定义如普通函数:
void fr(A a){cout<<a.i<<endl;}//i为a的私有成员
//...
mutable 数据成员
如果类A内有一个mutable数据成员,那么不管A的对象是否是const的,该数据成员都可变:
struct B{
mutable int i = 100 ;
};
//...
const B b;
b.i = 250; //正确:i是mutable的
类可以只声明,不定义,此时该类是不完全类型,这个时候,可以定义指向这种类型的指针
除了静态成员外,只有类完成定义后,才能创建该类的对象,毕竟,只有类的成员是已知的,编译器才知道分配多少空间,怎样初始化对象。
测试代码:
#include <iostream>
using namespace std;
class A;
A *p;
struct A{
int i ;
static int j ;
};
int main()
{
A a[2];
p = a;
p->i = 1 ;
(p+1)->i = 2 ;
cout<<a[0].i<<" "<<a[1].i<<endl;
return 0;
}
注意,上述代码中没有定义static成员,但仍然能够定义和初始化对象。这是因为静态成员之和类类型关联,和对象无直接关系。
page 250 友元再探
不光函数可以定义为类的友元,还可以把其它类,其它类的成员函数定义为友元,类内声明示例:
//声明友元类
class A{
//A的定义
friend class B;
//A的其它定义
}
//令成员函数作为友元
class A{
//A的定义
friend double C::clear(int) ;
//A的其它定义
}
声明友元的时候,要协调好定义每个类出现的顺序,代码示例:
#include <iostream>
using namespace std;
class A;//因为定义C过程中成员函数g的形参的类型是A,所以要声明它,但不必定义
struct C{ //因为下面A声明了void C::g(A) ,所以要定义类C,但成员函数g没必要定义
void g(A a);
};
struct A{
friend class B; //这里应该包含了B的声明,所以前面不需要单独对B进行声明
friend void C::g(A);
private:
int i = 250;
};
struct B{
void f(A a){cout<<"B: "<<a.i<<endl;}
};
void C::g(A a){cout<<"C: "<<a.i<<endl;}
int main()
{
A a;
B b;
C c;
b.f(a);
c.g(a);
return 0;
}
page252 作用域
struct A{
typedef int *P;
P p;
};
P p ; //错误,P离开了它的作用域A
A::P q;//正确
page254 查找规则
普通作用域查找规则:
int i = 1;
{
int j = i ;
int i = 2 ;
int k = i ;
cout<<j<<" "<<i<<endl;
}
这个时候,j = 1 ,即外层作用域的i , k = 2 ;
然而,类的查找规则略有不同,这是因为类是先处理声明,直到类成员全部可见后,再处理函数体的。因此,代码
int i = 1 ;
struct A{
void f(){cout<<i<<endl;}
private:
const int i = 2;
};
int main()
{
A a;
a.f();
return 0;
}
结果将输出2.
注记:如果在类外部定义成员函数,如果一个全局函数出现在该成员函数之前,并且出现在类的定义之后,那么,这个全局函数使可以被该成员函数利用的
page264 隐式转换
接受某个实参的构造函数有时也叫做转换构造函数,这是因为它可以实现隐式转换,例如:
struct A{
A() = default;
A(const A &rhs): i(rhs.i){cout<<"copy constructor"<<endl;}
A(int k): i(k) {cout<<"convert"<<endl;}
int i ;
};
void test(A a){
cout<<a.i<<endl;
}
在上述定义下,如果我们有如下调用语句:
test(250) ;
编译器受限利用250构造一个临时对象,这时它将调用A(int)构造函数,因此它会输出“convert” , 然后,输出a.i,即250.值得注意的是,它并没有调用构造函数A(const A&)
另外还请注意,如果构造函数是非常量引用: A(int&),毕竟,我们不能是一个非常量引用指向临时对象。。。
然而,常量引用是可以滴:A(const int&);
关于explicit,有如下说明
1.如果要阻止隐式转换,则在声明前加上explicit,例如:
explicit A(const A &rhs): i(rhs.i){cout<<"copy constructor"<<endl;}
2.当构造函数有多个形参时,不能执行隐式初始化,因此我们也就没有必要对这种构造函数进行explicit声明
3.隐式转换只能用在直接初始化,拷贝初始化则禁止隐式转换,例如,尽管拷贝构造函数是允许隐式转换的,但如下拷贝初始化语句仍然不能通过编译:
//定义Sales_data类的代码:
...
Sales_data( const Sales_data&);
Sales_data(string);
...
//定义Sales_data类的对象:
string null_book
Sales_data a = null_book ; //错误:拷贝构造函数不能隐式转换