Các lớp và đối tượng: tôi thực sự cần bao nhiêu và loại tệp nào để sử dụng chúng?


20

Tôi không có kinh nghiệm trước đây với C ++ hoặc C, nhưng biết cách lập trình C # và đang học Arduino. Tôi chỉ muốn tổ chức các bản phác thảo của mình và khá thoải mái với ngôn ngữ Arduino ngay cả với những hạn chế của nó, nhưng tôi thực sự muốn có một cách tiếp cận hướng đối tượng vào lập trình Arduino của tôi.

Vì vậy, tôi đã thấy rằng bạn có thể có các cách sau (không phải danh sách đầy đủ) để tổ chức mã:

  1. Một tập tin .ino duy nhất;
  2. Nhiều tệp .ino trong cùng một thư mục (những gì IDE gọi và hiển thị như "tab");
  3. Một tệp .ino có tệp .h và .cpp đi kèm trong cùng một thư mục;
  4. Tương tự như trên, nhưng các tệp là một thư viện được cài đặt bên trong thư mục chương trình Arduino.

Tôi cũng đã nghe nói về những cách sau, nhưng chúng vẫn chưa hoạt động:

  • Khai báo một lớp kiểu C ++ trong cùng một tệp .ino duy nhất (đã nghe nói, nhưng chưa bao giờ thấy hoạt động - điều đó thậm chí có thể không?);
  • [cách tiếp cận ưa thích] Bao gồm tệp .cpp trong đó một lớp được khai báo, nhưng không sử dụng tệp .h (muốn cách tiếp cận này, nó có hoạt động không?);

Lưu ý rằng tôi chỉ muốn sử dụng các lớp để mã được phân vùng nhiều hơn, các ứng dụng của tôi phải rất đơn giản, chỉ liên quan đến các nút, led và buzzers.


Đối với những người quan tâm, có một cuộc thảo luận thú vị về không đầu (chỉ cpp) định nghĩa lớp ở đây: programmers.stackexchange.com/a/35391/35959
heltonbiker

Câu trả lời:


31

IDE tổ chức mọi thứ như thế nào

Điều đầu tiên, đây là cách IDE tổ chức "bản phác thảo" của bạn:

  • Tệp chính .inolà một trong cùng tên với thư mục mà nó nằm trong. Vì vậy, foobar.inotrong foobarthư mục - tệp chính là foobar.ino.
  • Bất kỳ .inotệp nào khác trong thư mục đó được nối với nhau, theo thứ tự chữ cái, ở cuối tệp chính (bất kể tệp chính nằm ở đâu, theo thứ tự bảng chữ cái).
  • Tệp được nối này trở thành một .cpptệp (ví dụ. foobar.cpp) - nó được đặt trong thư mục biên dịch tạm thời.
  • Bộ tiền xử lý "hữu ích" tạo các nguyên mẫu hàm cho các hàm mà nó tìm thấy trong tệp đó.
  • Các tập tin chính được quét cho các #include <libraryname>chỉ thị. Điều này kích hoạt IDE cũng sao chép tất cả các tệp có liên quan từ mỗi thư viện (được đề cập) vào thư mục tạm thời và tạo hướng dẫn để biên dịch chúng.
  • Bất kỳ .c, .cpphoặc .asmcác tệp trong thư mục phác thảo đều được thêm vào quy trình xây dựng dưới dạng các đơn vị biên dịch riêng biệt (nghĩa là chúng được biên dịch theo cách thông thường dưới dạng các tệp riêng biệt)
  • Bất kỳ .htệp nào cũng được sao chép vào thư mục biên dịch tạm thời, do đó chúng có thể được gọi bằng các tệp .c hoặc .cpp của bạn.
  • Trình biên dịch thêm vào các tệp tiêu chuẩn quy trình xây dựng (như main.cpp)
  • Quá trình xây dựng sau đó biên dịch tất cả các tệp trên thành tệp đối tượng.
  • Nếu giai đoạn biên dịch thành công, chúng được liên kết với nhau cùng với các thư viện chuẩn AVR (ví dụ: cung cấp cho bạn, strcpyv.v.)

Một tác dụng phụ của tất cả những điều này là bạn có thể coi bản phác thảo chính (các tệp .ino) là C ++ cho tất cả các ý định và mục đích. Tuy nhiên, việc tạo nguyên mẫu hàm có thể dẫn đến các thông báo lỗi tối nghĩa nếu bạn không cẩn thận.


Tránh các quirks tiền xử lý

Cách đơn giản nhất để tránh những điều bình dị này là để trống bản phác thảo chính của bạn (và không sử dụng bất kỳ .inotệp nào khác ). Sau đó tạo một tab khác (một .cpptệp) và đặt nội dung của bạn vào đó như thế này:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Lưu ý rằng bạn cần bao gồm Arduino.h. IDE thực hiện điều đó tự động cho bản phác thảo chính, nhưng đối với các đơn vị biên dịch khác, bạn phải thực hiện nó. Mặt khác, nó sẽ không biết về những thứ như String, các thanh ghi phần cứng, v.v.


Tránh các thiết lập / mô hình chính

Bạn không phải chạy với khái niệm thiết lập / vòng lặp. Ví dụ: tệp .cpp của bạn có thể là:

#include <Arduino.h>

int main ()
  {
  init ();  // initialize timers
  Serial.begin (115200);
  Serial.println ("Hello, world");
  Serial.flush (); // let serial printing finish
  }  // end of main

Buộc thư viện bao gồm

Nếu bạn chạy với khái niệm "phác thảo trống", bạn vẫn cần bao gồm các thư viện được sử dụng ở nơi khác trong dự án, ví dụ như trong .inotệp chính của bạn :

#include <Wire.h>
#include <SPI.h>
#include <EEPROM.h>

Điều này là do IDE chỉ quét tệp chính để sử dụng thư viện. Thực tế, bạn có thể coi tệp chính là tệp "dự án" chỉ định thư viện bên ngoài nào đang được sử dụng.


Vấn đề đặt tên

  • Đừng đặt tên cho bản phác thảo chính của bạn là "main.cpp" - IDE bao gồm main.cpp của chính nó để bạn có một bản sao nếu bạn làm điều đó.

  • Không đặt tên tệp .cpp của bạn có cùng tên với tệp .ino chính của bạn. Vì tệp .ino thực sự trở thành tệp .cpp, điều này cũng sẽ cung cấp cho bạn một xung đột tên.


Khai báo một lớp kiểu C ++ trong cùng một tệp .ino duy nhất (đã nghe nói, nhưng chưa bao giờ thấy hoạt động - điều đó thậm chí có thể không?);

Vâng, điều này biên dịch OK:

class foo {
  public:
};

foo bar;

void setup () { }
void loop () { }

Tuy nhiên, bạn có lẽ tốt nhất nên tuân theo thông lệ thông thường: Đặt các khai báo của bạn trong .hcác tệp và định nghĩa (triển khai) của bạn trong .cpp(hoặc .c) tệp.

Tại sao "có thể"?

Như ví dụ của tôi cho thấy bạn có thể đặt mọi thứ lại với nhau trong một tệp. Đối với các dự án lớn hơn là tốt hơn để được tổ chức nhiều hơn. Cuối cùng, bạn lên sân khấu trong một dự án có quy mô vừa và lớn, nơi bạn muốn tách những thứ thành "hộp đen" - nghĩa là, một lớp làm một việc, làm tốt, được kiểm tra và khép kín ( càng xa càng tốt).

Nếu lớp này sau đó được sử dụng trong nhiều tệp khác trong dự án của bạn thì đây là nơi riêng biệt .h.cppcác tệp phát huy tác dụng.

  • Các .htập tin tuyên bố lớp - có nghĩa là, nó cung cấp đủ chi tiết cho các tập tin khác để biết những gì nó làm, những gì các chức năng nó có, và cách chúng được gọi.

  • Các .cpptập tin định nghĩa (dụng cụ) lớp - có nghĩa là, nó thực sự cung cấp các chức năng, và các thành viên lớp tĩnh, mà làm cho lớp làm điều này. Vì bạn chỉ muốn thực hiện nó một lần, đây là một tệp riêng biệt.

  • Các .htập tin là những gì được bao gồm trong các tập tin khác. Các .cpptập tin được biên dịch một lúc bằng cách IDE để thực hiện các chức năng lớp.

Thư viện

Nếu bạn theo mô hình này, thì bạn đã sẵn sàng để chuyển toàn bộ lớp ( tệp .h.cpptệp) vào một thư viện rất dễ dàng. Sau đó, nó có thể được chia sẻ giữa nhiều dự án. Tất cả những gì được yêu cầu là để tạo ra một thư mục (ví dụ. myLibrary) Và đặt .h.cppcác file trong nó (ví dụ. myLibrary.hmyLibrary.cpp) và sau đó đặt thư mục này bên trong của bạn librariesthư mục trong thư mục mà phác thảo của bạn được lưu giữ (thư mục quyển phác thảo).

Khởi động lại IDE và bây giờ nó biết về thư viện này. Điều này thực sự đơn giản và bây giờ bạn có thể chia sẻ thư viện này qua nhiều dự án. Tôi làm điều này rất nhiều.


Một chút chi tiết ở đây .


Câu trả lời tốt đẹp. Tuy nhiên, một chủ đề quan trọng nhất vẫn chưa trở nên rõ ràng với tôi: tại sao mọi người nói rằng "bạn có lẽ tốt nhất nên tuân theo thực tiễn thông thường: .h + .cpp"? Tại sao nó tốt hơn? Tại sao có lẽ là một phần? Và quan trọng nhất: làm thế nào tôi không thể làm điều đó, nghĩa là có cả giao diện và cách triển khai (đó là, toàn bộ mã lớp) trong cùng một tệp .cpp? Cảm ơn bạn rất nhiều vì bây giờ! : o)
heltonbiker

Đã thêm một vài đoạn để trả lời tại sao "có lẽ" bạn nên có các tệp riêng biệt.
Nick Gammon

1
Làm thế nào để bạn không làm điều đó? Chỉ cần đặt tất cả chúng lại với nhau như minh họa trong câu trả lời của tôi, tuy nhiên bạn có thể thấy rằng bộ tiền xử lý hoạt động chống lại bạn. Một số định nghĩa lớp C ++ hoàn toàn hợp lệ không thành công nếu chúng được đặt trong tệp .ino chính.
Nick Gammon

Chúng cũng sẽ thất bại nếu bạn bao gồm tệp .H trong hai tệp .cpp của mình và tệp .h đó chứa mã, đây là thói quen phổ biến của một số người. Nguồn mở của nó, chỉ cần sửa nó. Nếu bạn không thoải mái khi làm điều đó, có lẽ bạn không nên sử dụng nguồn mở. Giải thích tuyệt vời @Nick Gammon, tốt hơn bất cứ điều gì tôi đã thấy cho đến nay.

@ Spiked3 Không phải là vấn đề lựa chọn những gì tôi cảm thấy thoải mái nhất, bây giờ, vấn đề là biết những gì có sẵn cho tôi để lựa chọn ngay từ đầu. Làm thế nào tôi có thể đưa ra lựa chọn hợp lý nếu tôi thậm chí không biết lựa chọn của mình là gì và tại sao mỗi lựa chọn lại như vậy? Như tôi đã nói, tôi không có kinh nghiệm trước đây về C ++ và có vẻ như C ++ trong Arduino có thể cần được chăm sóc thêm, như thể hiện trong câu trả lời này. Nhưng tôi chắc chắn cuối cùng tôi cũng nắm bắt được nó và hoàn thành công việc của mình mà không cần phát minh lại bánh xe (ít nhất là tôi hy vọng vậy) :)
heltonbiker 7/07/2015

6

Lời khuyên của tôi là hãy tuân theo cách làm việc điển hình của C ++: giao diện riêng và thực hiện thành các tệp .h và .cpp cho mỗi lớp.

Có một vài lưu ý:

  • bạn cần ít nhất một tệp .ino - Tôi sử dụng một liên kết tượng trưng đến tệp .cpp nơi tôi khởi tạo các lớp.
  • bạn phải cung cấp các cuộc gọi lại mà môi trường Arduino mong đợi (setu, loop, v.v.)
  • trong một số trường hợp, bạn sẽ ngạc nhiên bởi những điều kỳ lạ không chuẩn giúp phân biệt Arduino IDE với một bình thường, như tự động bao gồm các thư viện nhất định, nhưng không phải là các thư viện khác.

Hoặc, bạn có thể bỏ Arduino IDE và thử với Eclipse . Như tôi đã đề cập, một số điều được cho là giúp người mới bắt đầu, có xu hướng đến theo cách của các nhà phát triển có kinh nghiệm hơn.


Mặc dù tôi cảm thấy việc tách một bản phác thảo thành nhiều tệp (tab hoặc bao gồm) giúp mọi thứ ở đúng vị trí của nó, tôi cảm thấy cần phải có hai tệp để xử lý cùng một thứ (.h và .cpp) là một loại dự phòng / sao chép không cần thiết. Có cảm giác như lớp học được xác định hai lần và mỗi khi tôi cần thay đổi một nơi, tôi cần thay đổi nơi khác. Lưu ý điều này chỉ áp dụng cho các trường hợp đơn giản như của tôi, trong đó sẽ chỉ có một triển khai của một tiêu đề nhất định và chúng sẽ chỉ được sử dụng một lần (trong một bản phác thảo).
heltonbiker

Nó đơn giản hóa công việc của trình biên dịch / trình liên kết và cho phép bạn có trong các phần tử tệp .cpp không phải là một phần của lớp, nhưng được sử dụng trong một số phương thức. Và trong trường hợp lớp có các bộ nhớ tĩnh, bạn không thể đặt chúng trong tệp .h.
Igor Stoppa

Việc tách các tệp .h và .cpp từ lâu đã được công nhận là không cần thiết. Java, C #, JS không yêu cầu các tệp tiêu đề và thậm chí các tiêu chuẩn iso cpp đang cố gắng tránh xa chúng. Vấn đề là có quá nhiều mã kế thừa có thể phá vỡ với một sự thay đổi căn bản như vậy. Đó là lý do chúng tôi có CPP sau C, và không chỉ là mở rộng C. Tôi mong điều tương tự sẽ xảy ra lần nữa, CPX sau CPP?

Chắc chắn, nếu phiên bản tiếp theo đưa ra cách thực hiện các nhiệm vụ tương tự được thực hiện bởi các tiêu đề, không có tiêu đề ... nhưng trong khi đó, có rất nhiều điều không thể thực hiện được nếu không có tiêu đề: Tôi muốn xem cách biên dịch phân tán có thể xảy ra mà không có tiêu đề và không phát sinh chi phí lớn.
Igor Stoppa

6

Tôi đang đăng một câu trả lời chỉ cho đầy đủ, sau khi tìm hiểu và thử nghiệm cách khai báo triển khai một lớp trong cùng một tệp .cpp mà không cần sử dụng tiêu đề. Vì vậy, liên quan đến cụm từ chính xác cho câu hỏi của tôi "tôi cần bao nhiêu loại tệp để sử dụng các lớp", câu trả lời hiện tại sử dụng hai tệp: một .ino với một bao gồm, thiết lập và vòng lặp và .cpp chứa toàn bộ (khá tối giản ) lớp, đại diện cho các tín hiệu rẽ của một chiếc xe đồ chơi.

Blinker.ino

#include <TurnSignals.cpp>

TurnSignals turnSignals(2, 4, 8);

void setup() { }

void loop() {
  turnSignals.run();
}

TurnSignals.cpp

#include "Arduino.h"

class TurnSignals
{
    int 
        _left, 
        _right, 
        _buzzer;

    const int 
        amberPeriod = 300,

        beepInFrequency = 600,
        beepOutFrequency = 500,
        beepDuration = 20;    

    boolean
        lightsOn = false;

    public : TurnSignals(int leftPin, int rightPin, int buzzerPin)
    {
        _left = leftPin;
        _right = rightPin;
        _buzzer = buzzerPin;

        pinMode(_left, OUTPUT);
        pinMode(_right, OUTPUT);
        pinMode(_buzzer, OUTPUT);            
    }

    public : void run() 
    {        
        blinkAll();
    }

    void blinkAll() 
    {
        static long lastMillis = 0;
        long currentMillis = millis();
        long elapsed = currentMillis - lastMillis;
        if (elapsed > amberPeriod) {
            if (lightsOn)
                turnLightsOff();   
            else
                turnLightsOn();
            lastMillis = currentMillis;
        }
    }

    void turnLightsOn()
    {
        tone(_buzzer, beepInFrequency, beepDuration);
        digitalWrite(_left, HIGH);
        digitalWrite(_right, HIGH);
        lightsOn = true;
    }

    void turnLightsOff()
    {
        tone(_buzzer, beepOutFrequency, beepDuration);
        digitalWrite(_left, LOW);
        digitalWrite(_right, LOW);
        lightsOn = false;
    }
};

1
Điều này giống như java và tát việc thực hiện các phương thức vào khai báo của lớp. Ngoài khả năng đọc giảm - tiêu đề cung cấp cho bạn khai báo các phương thức ở dạng ngắn gọn - tôi tự hỏi liệu các khai báo lớp khác thường hơn (như với statics, bạn bè, v.v.) có còn hoạt động không. Nhưng hầu hết các ví dụ này không thực sự tốt, bởi vì nó chỉ bao gồm tệp một lần bao gồm đơn giản là nối. Các vấn đề thực sự bắt đầu khi bạn bao gồm cùng một tệp ở nhiều nơi và bạn bắt đầu nhận được các khai báo đối tượng xung đột từ trình liên kết.
Igor Stoppa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.