C++ primer 笔记 1~7 章

来源:互联网 发布:js数组重排序 编辑:程序博客网 时间:2024/06/13 22:49


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离开了它的作用域AA::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_bookSales_data a = null_book ; //错误:拷贝构造函数不能隐式转换




0 0
原创粉丝点击