分类 编程语言 下的文章

模板是C++中一个重要的部分,也是泛型编程的基础,一个模板就是一个创建类或函数的蓝图,使用它在很多时候都能给我们带来巨大的便利。

一、函数模板

定义一个函数模板的语法为:

template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    else if (v1 > v2) return 1;
    else return 0;
}

模板以关键字template 开始,后面跟一个模板参数列表,和函数的形参一样,参数列表可以是多个以逗号隔开的一个或多个参数,外部用<> 包起来。

T 代表一种数据类型,可以为任意的内置类型或自定义类,只要该类型实现了函数重载,就可以通过compare() 函数进行比较。

- 阅读剩余部分 -

0x01 介绍

C++类中有两种方式可以用来初始化成员变量,一种最常见的是在构造函数内部直接对成员函数赋值:

class CTest{
    int m_a, m_b;
    CTest(int a, int b){
        m_a = a;
        m_b = b;
    }
}

另外一种方式就是通过构造函数的初始值列表来完成初始化:

class CTest{
    int m_a, m_b;
    CTest(int a, int b) : m_a(a), m_b(b){}
}

这种在构造函数后加一个冒号然后初始化的方式叫做构造函数初始值列表,它更优于第一种初始化方式。

0x02 初始值列表的必要性

在以下情况下,必需使用初始值列表方式来初始化:

  • 类成员包含const对象时。
  • 在类A没有提供默认构造函数且被类B包含或者继承时,类B必需使用默认初始化方式初始化A。

对于第一个条件比较好理解,因为const本身在初始化后是无法再赋值的,所以必须使用初始化列表来对其初始化。

对于第二种情况,因为根据构造函数的执行顺序,在构造类B时必须先构造类A的构造函数,但类A并没有提供默认的构造函数,此时导致编译器找不到合适的构造函数,所以对象构造失败。因此这里必须在初始值列表中初始化。

#pragma once
#include <iostream>
class CAnimal
{
public:
    CAnimal(int weight) :m_weight(weight) {
    }
    int m_weight;
};

class CDog {
public:
    CAnimal m_a;
    const int m_b;
    CDog(int a, int b) : m_b(b), m_a(a){
    }
};

0x02 初始值列表的效率

使用普通方式初始化时编译器会先执行一次对象的默认构造函数,然后才会对其赋值,初始化时执行了两个步骤。

#include <iostream>
class CAnimal
{
public:
    CAnimal() { std::cout << "default" << std::endl; }  // 添加默认初始化函数
    CAnimal(int weight) :m_weight(weight) {
    }
    int m_weight;
};

class CDog : public CAnimal {
public:
    CDog(int weight) {
        m_weight = weight;
    }
    // 初始值列表,和上面的构造函数不可共存
    CDog(int weight): CAnimal(weight) {
    }
};

执行CDog构造函数时会先执行CAnimal的默认构造函数,输出defualt 然后后才会执行m_weight = weight。但是对于初始值列表方式来说并不会执行两步,直接通过相应的构造函数初始化就完成了,相对来说简化了一个过程,效率肯定也会高一些。

0x03 初始化顺序

使用初始值列表初始化时,初始化的顺序是根据成员变量定义的顺序来的,并不是初始值列表的顺序。

class CAnimal
{
public:
    CAnimal(int weight) :m_weight(weight) {
        std::cout << weight << std::endl;
    }
    int m_weight;
};

class CDog {
public:
    CAnimal m_a, m_b;
    CDog(int a, int b) : m_b(b), m_a(a){
    }
};

实例一个对象CDog d(1, 2) ,结果会输出:

1
2

并不是根据初始值列表先用2初始化m_b,可见初始顺序是根据定义顺序来的。

很久没有用C++,今天用C++写链表,结果因为一个小问题卡了好半天。

浪费了大半天才找到问题的原因,这里记录一下,生疏了。。。

创建一个类CTest ,代码如下:

#pragma once
class CTest
{
public:
    CTest() { i = 10; };
    ~CTest() { i = 0; };
    int i;
};

主函数中创建一个CTest 指针并打印i 值:

#include <iostream>
#include "Test.h"

int main() {
    CTest *t = &CTest();
    std::cout << t->i << std::endl;
    return 0;
}

最后的结果是0 不是10 ,断点调试发现t 的地址和CTest() 构造的时候一致:

// CTest.h
CTest() {
    i = 10;
    std::cout << (void*)this << " " << i <<  std::endl;
};

// main.cpp
CTest *t = &CTest();
cout << (void*)t << endl;

打印结果:

00FBFA8C 10
00FBFA8C
0

t.i 在构造的时候确实赋值了,并且t 的地址在返回前后没有变,这也说明构造t 在初始化时并没是拷贝之后再返回的。那为什么t.i 在初始化之后变成了0 ???

1. 错误原因

研究一番发现,使用CTest *t = &CTest() 形式创建的变量,虽然没有拷贝到其他副本再返回,但在返回赋值到t 前会执行一次析构,这就导致了t.i 又重新被赋值成了0 。这里可以修改修改~CTest() 方法验证这个观点:

~CTest() { 
    std::cout << "~CTest()" << std::endl;
    i = 0;
};

运行结果:

004FFCDC
~CTest()
004FFCDC
0

可见析构确实是被运行了,所以结果也确实应该是0!

2. 改进方法

创建指针时,使用new 创建而不要用上面的方式创建,使用new 创建不会出现上面的问题。

3. 吐槽

go 写久了,c 都忘得差不多了,都忘了还有new 这个东西了。。。

一、waitgroup介绍

多线程编程中,经常会遇到这样的一种场景:main函数中为了等待其他线程执行完,在return之前都要执行sleep以争取更多的时间给其他线程执行。例如:

package main

import (
    "fmt"
    "time"
)

func main(){
    for i := 0; i < 100 ; i++{
        go fmt.Println(i)
    }
    time.Sleep(time.Second)
}

主线程为了等待goroutine都运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,100个for循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果。但是对于实际场景来说,大多无法预知for循环内代码运行时间的长短,因此1秒可能是不够的。所以睡眠也就达不到我们想要的效果。

那么我们可能又会想到使用管道来完成同步,因为管道本身就是用来作为数据通信使用的,用在此处也合理。于是就有了这样的代码:

unc main() {
    c := make(chan bool, 100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            c <- true
        }(i)
    }

    for i := 0; i < 100; i++ {
        <-c
    }
}

首先可以肯定的是使用管道是能达到我们的目的的,但是问题是管道用在这里真的合适吗?管道是go中用来给多个线程(协程)间通信的,使用它来仅仅作为状态同步,是不是有点大材小用了。而且,管道是基于共享内存实现的,假设我们有一万、十万甚至更多的for循环,也要申请同样数量大小的管道出来,对系统性能也会造成更多的负载。

WaitGroup(中文名叫等待组)就是用来解决这种问题的,比较适合统筹多个协程间都到达某个状态。例如,等待所有协程都执行完这个状态。在WaitGroup 对象实现中,内部有一个计数器,最初从0开始,它有三个方法:

  • Add():计数器加一
  • Done():计数器减一
  • Wait():等待计数器清零

执行Wait方法的函数在等待组内部计数器不为0的时候回阻塞,一旦计数器为0了,程序就会继续往下执行。

利用WaitGroup实现上面的代码:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

程序刚开始首先把wg 计数设置为100,然后开启100个协程执行任务,每个协程执行完成之后把计数器减1,主线程中等待计数清零。当wg.Wait()继续向下执行了,也就说明所有的协程都执行完了。相较于管道来说,WaitGroup更简单,也更轻量。

二、注意事项

2.1 计数器不能为负值

使用等待组时注意不能通过Add()wg 设置一个负值,否则代码将会报错:

panic: sync: negative WaitGroup counter

goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc000014060, 0xffffffffffffffff)
        /usr/local/go/src/sync/waitgroup.go:74 +0x139
main.main()
        /Users/maqian/code/go/src/awesomeProject/waitgroup/waitgroup.go:10 +0x4d

同样使用Done()也要特别注意不要把计数器设置成负数了。

2.2 WaitGroup对象不是一个引用类型

WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址:

// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) {
    fmt.Println(i)
    wg.Done()
}

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}

0x01 以下代码能通过编译吗

package main

import "fmt"

type user interface {
    say(string)
}

type man struct{}

func (p *man) say(hello string) {
    fmt.Println(hello)
}

func main() {
    var u user = man{}
    u.say("Hello World")
}

不能通过编译,因为类型man没有实现user接口,实现say方法的是*man类型,两者不能统一。

func (p *man) say(hello string) 改成func (p man) say(hello string) 即可。

0x01 什么是逃逸

第一次听说逃逸是在雨痕学堂,一脸懵逼的百度了半天也没找到一个明确的说法,直到昨天在论坛上看到一篇关于变量逃逸的文章才明白。

因为函数都是运行在栈上的,在栈声明临时变量分配内存,函数运行完毕再回收该段栈空间,并且每个函数的栈空间都是独立的,其他代码都是不可访问的。

但是在某些情况下,栈上的空间需要在该函数被释放后依旧能访问到,这时候就涉及到内存的逃逸了。

- 阅读剩余部分 -