C++读书笔记

名称空间:

可能一个程序使用的两个组件,都含有名为wanda()的函数,此时需要用名称空间指出使用的是哪个版本的wanda()
例如有A和B两个组件都含有wanda()函数
using namespace A; wanda();  ==  A::wanda()

函数:

函数原型

为什么需要原型?

1.告诉编译器调用这个函数需要什么参数,没提供的话就报错
2.函数返回值会被放在指定的位置(CPU寄存器或者内存中),需要告诉编译器检索多少字节以及如何解释他们

编译器为什么不直接去找函数实现要这些信息?

1.效率不高
2.实现可能不在文件中,而在别的文件,编译器编译的时候可能无权访问那些文件。比如编译了a.c,但用到的实现在b.c
  如果函数是位于库中,情况也是如此。

函数参数

注意事项

传递了一个参数,但类型不正确怎么办?
C语言:造成奇怪的错误。例如需要int但传了double,则函数将只检查64位的前32位,并试图将他们解释为一个int
C++:   自动将传递的值转换为指定的类型,条件是两者都是算术类型,像整型转为指针之类的无意义操作,C++也不会强制转换

如果参数只是拿来使用而不修改
使用const修饰
const int *p     指向的int值不能修改
int * const p    p指向的地址不能修改

结构体作为参数:
直接传值:一般不直接拿结构体做参数,而是传它的地址,因为当结构体很大时复制结构体将增加内存要求,降低系统运行速度。

 

引用变量

引用变量是已有变量的一个别名,引用变量或者已有变量都可修改值。
引用变量的主要作用是作为函数的形参,通过将引用变量用作参数,函数将使用原始数据而不是副本。
这样除了指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。

 

创建引用变量

int rats;
int & rodents = rats;   //创建rats的引用变量
rodents++;              //应用变量的使用方式

 

引用和指针的区别
必须在声明引用时将其初始化,而不能像指针那样先声明再赋值。这也意味着一旦引用,无法修改引用的对象

int rats,test;
int & rodents = rats;   //创建rats的引用变量
rodents = test;         //效果并不是rodents变成了test的引用,而是将test的值赋给了rodents所引用的地址

 

引用作为参数

void swap1(int &a,int &b);
void swap2(int a,int b);

void swap1(int &a,int &b)
{
    int tmp;
    
    tmp = a;
    a = b;
    b = tmp;
}

void swap2(int a,int b)
{
    int tmp;
    
    tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int a=0,b=1;
    swap1(a,b);     //能成功交换
    swap2(a,b);     //交换失败
    return 0;
}

可以看到,引用和直接传值的唯一差别只是函数申明,但效果却完全不同(成功交换和不成功)

 

引用作为返回值

为什么要返回引用

int & add(int &a,const int &b)
{
    a+=b;
    return a;
}
int c = add(6,7);

普通函数返回值需要先放到一个临时位置,再从这个临时位置赋给c。
返回引用的话讲直接从a赋值给c,效率更高。

 

返回引用注意事项

不能返回已经不存在的内存单元的引用,比如:

int & add(int &a,const int &b)
{
    int c;
    c = a+b;
    return c;   //错误
}

由于c是个局部变量,函数结束后就被销毁了,因此在函数外试图使用它的引用是错误的。

 

默认参数

假设有一个函数left,用于返回str的从左往右的前n个字符组成的字符串

char *left(const char *str,int n);

当用户经常只需要使用n=1的场景,偶尔才使用n>1的场景时,C++提供了默认参数这个新内容

char *left(const char *str,int n = 1);  //只需要在声明处写默认值
char *left(const char *str,int n )       //实现不用写默认值
{
    ...
} 

当不填写n时,默认n=1,填写了n时,使用填写的值
left("name")  == left("name",1)

 

注意事项:

当有多个参数时,必须从右向左添加默认值,也就是说你想为某个参数设默认值,必须先给它右边的所有参数设默认值。
毕竟,像下面这样的函数调用还是不能接受的
func(1,,2);

 

内联函数(inline)

普通函数的调用:先保存程序当前地址 =》 跳转到函数地址执行代码 =》结束后返回原先的地址
内联函数的调用:编译时直接在调用处展开函数代码 =》顺序执行
内联函数优点:内联函数的运行速度比常规函数稍快(省去跳转的时间)
内联函数缺点:占用他更多内存,比如10个地方调用,就会有10个函数备份
适用场景:函数体小且经常被调用,调用开销>运行开销
注意事项: ① inline只是给编译器的一个建议,最终作不作为内联函数由编译器决定。
                     内联函数不能递归。内联函数不能有循环,switch(编译器认为是复杂函数不能作为内联)
                 ② inline修饰的函数不支持跨文件,只在当前文件内有效。
                     即不建议声明和定义分离,分离会导致链接错误。一般写在头文件中。
                     inline被展开的时候,就没有函数地址了,链接就会出问题。
                 ③ 是基于实现,不是基于声明

inline void Foo(int x,int y); //inline与函数声明放在一起,不能成为lnline
void Foo(int x,int y){}

void Foo(int x,int y);
inline void Foo(int x,int y){} //与函数的定义体放在一起,可以成为内联函数

 

内联函数和宏的区别

inline 在编译阶段进行参数 类型检查和安全检查,宏处理在预编译期间,不进行参数 类型检查和安全检查。
nline是一种更安全的宏  

 

函数重载(函数多态)

函数重载指的是可以有多个同名的函数,通过函数的参数列表区分。

int func(int a);        #1
int func(char *b);      #2

func(1);     //调动#1
func("nihao")//调用#2

一些看起来彼此不同的函数是不能共存的:

double cube(double x);
double cube(double &x);

虽然看起来两者不一样,但是在编译器角度看,随便传个double参数进来,和double double &都匹配,
因此函数就不知道该用哪个函数了,编译器将类型引用和类型视为相同。
同样的,编译器也不区分const和非const。

 

什么时候使用函数重载:

在函数基本上执行相同的任务,但使用不同的数据时。

 

类中的函数(接口,方法)

公共接口(public函数):
让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。
比如,要计算string类的对象包含多少字符,不需要打开对象,只需要使用string类提供的size()接口

 

类声明示例:

(通常把数据成员放私有部分,成员函数放公有部分,使用者通过成员函数修改或访问成员变量)

class Stock
{
private:
    std::string company;    //股票公司
    long shares;            //股票数
    double share_val;       //股价
    double total_val;       //股票价值
    void set_tot() {total_val = shares*share_val;}              //就地实现的方式
public:
    void acquire(const std::string & co,long n,double pr);      //单使用声明的方式
    void buy(long num,double price);
    void sell(long num,double price);
    void update(double price);
    void show();
};

 

实现类成员函数(比如上文的update函数)特点:
定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类。
类成员函数可以访问类的private组件。

void Stock::update(double price)
{
    share_val = price;
    set_tot();
}

 

内联方法

其实现位于类声明中的函数将自动成为内联函数,因此Stock::set_tot()是一个内联函数。
类声明常将短小的成员函数作为内联函数,set_tot()符合这样的条件。

也可以在类声明之外定义成员函数,并使其成为内联函数:

inline void Stock::update(double price)
{
    share_val = price;
    set_tot();
}

//使用类的成员函数:
Stock k,l;
k.show();
l.show();

内联函数要求每个使用它的文件中都有它的实现代码。确保内联函数实现对多文件程序中的每个文件都可用,最简便的方法是:
将内联函数实现放在定义类的头文件中。

 

存储空间

创建的每个新对象都有自己的存储空间,用于存储其内部变量或者类成员。
但同一个类的所有对象共享同一组类函数。
比如k.shars和l.shars各占一个内存块,但k.show()和l.show()将执行同一个代码块

 

 构造函数

由于类存在私有变量,所以无法像int类型那样在创建对象时初始化

Stock hot = {"abc",200,50.25};  //编译错误

类的构造函数用于创建对象时,自动对它进行初始化。

 

构造函数声明和实现:
构造函数的声明和实现有些特殊的点:①名称和类名相同 ②没有返回值,也没有声明为void类型
以Stock类为例,需要为Stock类提供3个值(total_val通过set_tot()计算获得,不需要设置),
如果只需要初始化company的话,也可以使用之前说过的默认参数,则声明为:

Stock(const string & co,long n = 0,double pr = 0.0);

//构造函数的实现可能是这样的:
Stock::Stock(const string &co,long n,double pr)
{
    company = co;
    
    if(n<0)
        shares = 0;
    else
        shares = n;
    share_val = pr;
    set_tot();
}

注意没有返回类型,声明位于类声明的public部分。

 

那能不能图方便把实现中的参数名命名成和成员变量一样呢?

Stock::Stock(const string &company,long shares,double share_val) {...}

不能,因为在函数实现时你就会发现会出现shares = shares;这样的语句
为避免这种混轮,一种常见的做法是给参数加m_前缀,或者加_后缀:

Stock::Stock(const string &m_company,long m_shares,double m_share_val) {...}
Stock::Stock(const string &company_,long shares_,double share_val_) {...}

 

使用构造函数:

Stock food = Stock("World Cabbage",250,1.25);   //显式调用
Stock garment("Furry Mason",50,2.5);            //隐式调用
Stock *pStock = new Stock("Ele Games",18,19.0); //new时调用

 需要指出的是:无法通过对象调用构造函数。

 

默认构造函数

默认构造函数是在未提供显示初始值时,用来创建对象的构造函数,即下述情况:

Stock fluffy_the_cat;

 系统给的Stock的默认构造函数一般是:Stock::Stock() {}
需要注意的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
为类定义了构造函数后,程序员就必须为类再提供一个默认构造函数(而不是编译器自动提供),
否则会报错。

Stock(const string & co,long n = 0,double pr = 0.0); //单单定义自己的构造函数会报错

这样做可能是想禁止如下情况:创建了一个对象,而不显式地初始化。
 

Stock fluffy_the_cat;   //我找到了名为Stock的构造函数却发现套不进去

 

自己定义默认构造函数方法有两种:

一种是给已有构造函数的所有参数提供默认值:

Stock(const string &co = "error",long n = 0,double pr = 0.0);   //默认构造函数
//上述情况的话Stock fluffy_the_cat;就能套进去了

另一个是通过函数重载来定义另一个构造函数--一个没有参数的构造函数:

Stock(const string &co = "error",long n = 0,double pr = 0.0);   //构造函数
Stock();                                                        //默认构造函数

 

析构函数

 析构函数在对象过期时自动调用,完成清理工作。
析构函数的声明和实现有些特殊的点:名称为~类名,没有返回值,也没有声明为void类型,没有参数
因此Stock的析构函数声明必须是这样的:

~Stock();

对应的实现为:

Stock::~Stock()
{
    ...
}

如果程序员没指定析构函数,则编译器也会生成一个空的析构函数。

 

完善后的Stock类

class Stock
{
private:
    std::string company;    //股票公司
    long shares;            //股票数
    double share_val;       //股价
    double total_val;       //股票价值
    void set_tot() {total_val = shares*share_val;}    //就地实现的方式
public:
    Stock();    //默认构造函数
    Stock(const string &co,long n = 0,double pr = 0.0);     //构造函数
    ~Stock();   //析构函数
    void acquire(const std::string & co,long n,double pr);  //单使用声明的方式
    void buy(long num,double price);
    void sell(long num,double price);
    void update(double price);
    void show();
};

Stock::Stock()  //默认构造函数
{
    std::cout << "default constructor called \n";
    company = "no name";
    shares = 0;
    share_val = 0.0;
    total_val = 0.0;
}

Stock::Stock(const string &co,long n,double pr)
{
    std::cout << "constructor using" << co << "called \n";
    company = co;
    
    if(n < 0)
        shares = 0;
    else
        shares = n;
    share_val = pr;
    set_tot();
}

Stock::~Stock()
{
    std::cout << "Bye,"<<company << "!\n";
}

//其他函数的实现我就不写了

 

运算符重载

 前面讲了函数的重载,C++中运算符也可以重载。
比如*即可以在声明中用于定义指针,也可以作为运算的乘法符号。
那么,如果用户想自定义运算符的用法,该怎么做呢?
比如,将两个数组的元素逐个相加,一般需要用到循环:

for(int i=0;i<20;i++)
    evening[i] = sam[i] + janet[i];

但在C++中,可以定义一个代表数组的类,并重载+运算符:

evening = sam + janet;

重载运算符,需要用到被称为运算符函数的特殊函数形式,格式如下:

operatorop(argument list) //三部分operator+op+(argument list)

operator+()     //重载+运算符
operator*()     //重载*运算符

op必须是有效的运算符,不能有例如operator@()的函数,因为C++中没有@运算符
那么上述evening = sam + janet;可以这么实现:

#include<iostream>

class Stock
{
private:
    int num[20];
public:
    Stock(int a)
    {
        for(int i=0;i<20;i++)
            num[i] = a;
    }
    Stock(){};
    ~Stock(){};   //析构函数
    Stock operator+(const Stock &b)
    {
        Stock rt;
        for(int i=0;i<20;i++)
            rt.num[i] = this->num[i] + b.num[i];
        return rt;
    }
    int show()
    {
        std::cout << this->num[1] << std::endl;
    }
};

int main()
{
    Stock c;
    Stock a(1);
    Stock b(2);
    
    c = a + b;
    
    c.show();
    
    return 0;
}

执行c = a + b;时,编译器发现操作数是Stock类对象,于是执行了相应的运算符函数替换原运算符:
c = a.operator+(b);

 

友元函数

通常只有类的公有类函数能访问类的私有部分数据,C++提供了友元这种新的访问形式:
友元有三类:①友元函数 ②友元类 ③友元成员函数
此处讨论友元函数,通过让函数称为类的友元,可以赋予该函数与类成员函数相同的访问权限。

 

创建友元函数

将其声明放在类声明中,并在声明前加上关键字friend

friend int func(int a);

 该原型意味着两点:
1.虽然func()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
2.虽然func()函数不是成员函数,但它与成员函数的访问权限相同

函数实现(不需要加Stock::,也不需要使用friend):

int func(int a)
{
    ...//可以访问Stock的private数据
}

 

虚函数

指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。 

//举例:
#include<iostream>
using namespace std;
class A
{
    public:
        void print()
        {
            cout<<"This is A"<<endl;
        }
};
 
class B : public A
{
    public:
        void print()
        {
            cout<<"This is B"<<endl;
        }
};
 
int main()
{
    //为了在以后便于区分,我这段main()代码叫做main1
    A a;
    B b;
    a.print();
    b.print();
    return 0;
}

 执行后得到:
“This is A”、“This is B”。
可以看出这两个class因个体的差异而采用了不同的策略,但这并不是多态性行为(使用的是不同类型的指针),
没有用到虚函数的功能。现在把main()处的代码改一改。

int main()
{
    //main2
    A a;
    B b;
    A *p1 = &a;
    A *p2 = &b;
    p1->print();
    p2->print();
    return 0;
}

运行一下看看结果,结果却是两个This is A(错)。
问题来了,p2明明指向的是class B的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数。

class A
{
    public:
        virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
    public:
    void print(){cout<<"This is B"<<endl;}
};

毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?
回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。
所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
现在重新运行main2的代码,这样输出的结果就是This is A和This is B了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值